fix shell support in package attach (#2929)

* fix package attach

* final fixes

* apply changes to launch

* Update core/startos/src/service/effects/subcontainer/sync.rs
This commit is contained in:
Aiden McClelland
2025-05-07 09:19:38 -06:00
committed by GitHub
parent 68955c29cb
commit f6b4dfffb6
6 changed files with 573 additions and 217 deletions

View File

@@ -95,17 +95,15 @@ export class DockerProcedureContainer {
key, key,
) )
} else if (volumeMount.type === "pointer") { } else if (volumeMount.type === "pointer") {
await effects await effects.mount({
.mount({ location: path,
location: path, target: {
target: { packageId: volumeMount["package-id"],
packageId: volumeMount["package-id"], subpath: volumeMount.path,
subpath: volumeMount.path, readonly: volumeMount.readonly,
readonly: volumeMount.readonly, volumeId: volumeMount["volume-id"],
volumeId: volumeMount["volume-id"], },
}, })
})
.catch(console.warn)
} else if (volumeMount.type === "backup") { } else if (volumeMount.type === "backup") {
await subcontainer.mount(Mounts.of().addBackups(null, mounts[mount])) await subcontainer.mount(Mounts.of().addBackups(null, mounts[mount]))
} }

48
core/Cargo.lock generated
View File

@@ -4034,6 +4034,12 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]]
name = "numtoa"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6aa2c4e539b869820a2b82e1aef6ff40aa85e65decdd5185e83fb4b1249cd00f"
[[package]] [[package]]
name = "object" name = "object"
version = "0.32.2" version = "0.32.2"
@@ -4630,6 +4636,15 @@ version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac"
[[package]]
name = "pty-process"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8277b026e63da5d2cc435f842b52bedb1d050dfd7d633bba009c3c8e1883a21e"
dependencies = [
"rustix 0.38.44",
]
[[package]] [[package]]
name = "publicsuffix" name = "publicsuffix"
version = "2.3.0" version = "2.3.0"
@@ -4856,6 +4871,12 @@ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
] ]
[[package]]
name = "redox_termios"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20145670ba436b55d91fc92d25e71160fbfbdd57831631c8d7d36377a476f1cb"
[[package]] [[package]]
name = "redox_users" name = "redox_users"
version = "0.4.6" version = "0.4.6"
@@ -5124,6 +5145,7 @@ checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
"errno 0.3.11", "errno 0.3.11",
"itoa",
"libc", "libc",
"linux-raw-sys 0.4.15", "linux-raw-sys 0.4.15",
"windows-sys 0.59.0", "windows-sys 0.59.0",
@@ -6086,6 +6108,7 @@ dependencies = [
"procfs", "procfs",
"proptest", "proptest",
"proptest-derive", "proptest-derive",
"pty-process",
"qrcode", "qrcode",
"rand 0.9.0", "rand 0.9.0",
"regex", "regex",
@@ -6113,6 +6136,7 @@ dependencies = [
"sscanf", "sscanf",
"ssh-key", "ssh-key",
"tar", "tar",
"termion",
"textwrap", "textwrap",
"thiserror 1.0.69", "thiserror 1.0.69",
"tokio", "tokio",
@@ -6132,7 +6156,6 @@ dependencies = [
"tracing-subscriber", "tracing-subscriber",
"trust-dns-server", "trust-dns-server",
"ts-rs", "ts-rs",
"tty-spawn",
"typed-builder", "typed-builder",
"unix-named-pipe", "unix-named-pipe",
"url", "url",
@@ -6308,6 +6331,18 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "termion"
version = "4.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3669a69de26799d6321a5aa713f55f7e2cd37bd47be044b50f2acafc42c122bb"
dependencies = [
"libc",
"libredox",
"numtoa",
"redox_termios",
]
[[package]] [[package]]
name = "textwrap" name = "textwrap"
version = "0.16.2" version = "0.16.2"
@@ -6942,17 +6977,6 @@ dependencies = [
"termcolor", "termcolor",
] ]
[[package]]
name = "tty-spawn"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb91489cf2611235ae8d755d66ab028437980ee573e2230c05af41b136236ad1"
dependencies = [
"anyhow",
"nix 0.29.0",
"signal-hook",
]
[[package]] [[package]]
name = "tungstenite" name = "tungstenite"
version = "0.23.0" version = "0.23.0"

View File

@@ -39,7 +39,7 @@ path = "src/main.rs"
[features] [features]
cli = [] cli = []
container-runtime = ["procfs", "tty-spawn"] container-runtime = ["procfs", "pty-process"]
daemon = ["mail-send"] daemon = ["mail-send"]
registry = [] registry = []
default = ["cli", "daemon", "registry", "container-runtime"] default = ["cli", "daemon", "registry", "container-runtime"]
@@ -166,6 +166,7 @@ prettytable-rs = "0.10.0"
procfs = { version = "0.16.0", optional = true } procfs = { version = "0.16.0", optional = true }
proptest = "1.3.1" proptest = "1.3.1"
proptest-derive = "0.5.0" proptest-derive = "0.5.0"
pty-process = { version = "0.5.1", optional = true }
qrcode = "0.14.1" qrcode = "0.14.1"
rand = "0.9.0" rand = "0.9.0"
regex = "1.10.2" regex = "1.10.2"
@@ -197,6 +198,7 @@ sqlx = { version = "0.7.2", features = [
sscanf = "0.4.1" sscanf = "0.4.1"
ssh-key = { version = "0.6.2", features = ["ed25519"] } ssh-key = { version = "0.6.2", features = ["ed25519"] }
tar = "0.4.40" tar = "0.4.40"
termion = "4.0.5"
thiserror = "1.0.49" thiserror = "1.0.49"
textwrap = "0.16.1" textwrap = "0.16.1"
tokio = { version = "1.38.1", features = ["full"] } tokio = { version = "1.38.1", features = ["full"] }
@@ -217,7 +219,6 @@ tracing-journald = "0.3.0"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
trust-dns-server = "0.23.1" trust-dns-server = "0.23.1"
ts-rs = { git = "https://github.com/dr-bonez/ts-rs.git", branch = "feature/top-level-as" } # "8.1.0" ts-rs = { git = "https://github.com/dr-bonez/ts-rs.git", branch = "feature/top-level-as" } # "8.1.0"
tty-spawn = { version = "0.4.0", optional = true }
typed-builder = "0.18.0" typed-builder = "0.18.0"
unix-named-pipe = "0.2.0" unix-named-pipe = "0.2.0"
url = { version = "2.4.1", features = ["serde"] } url = { version = "2.4.1", features = ["serde"] }

View File

@@ -1,19 +1,22 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::ffi::{c_int, OsStr, OsString}; use std::ffi::{c_int, OsStr, OsString};
use std::fs::File; use std::fs::File;
use std::io::IsTerminal; use std::io::{IsTerminal, Read};
use std::os::unix::process::CommandExt; use std::os::unix::process::CommandExt;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::{Command as StdCommand, Stdio}; use std::process::{Command as StdCommand, Stdio};
use std::sync::Arc;
use nix::sched::CloneFlags; use nix::sched::CloneFlags;
use nix::unistd::Pid; use nix::unistd::Pid;
use signal_hook::consts::signal::*; use signal_hook::consts::signal::*;
use termion::raw::IntoRawMode;
use tokio::sync::oneshot; use tokio::sync::oneshot;
use tty_spawn::TtySpawn;
use crate::service::effects::prelude::*; use crate::service::effects::prelude::*;
use crate::service::effects::ContainerCliContext; use crate::service::effects::ContainerCliContext;
use crate::util::io::TermSize;
use crate::CAP_1_KiB;
const FWD_SIGNALS: &[c_int] = &[ const FWD_SIGNALS: &[c_int] = &[
SIGABRT, SIGALRM, SIGCONT, SIGHUP, SIGINT, SIGIO, SIGPIPE, SIGPROF, SIGQUIT, SIGTERM, SIGTRAP, SIGABRT, SIGALRM, SIGCONT, SIGHUP, SIGINT, SIGIO, SIGPIPE, SIGPROF, SIGQUIT, SIGTERM, SIGTRAP,
@@ -98,6 +101,10 @@ fn open_file_read(path: impl AsRef<Path>) -> Result<File, Error> {
pub struct ExecParams { pub struct ExecParams {
#[arg(long)] #[arg(long)]
force_tty: bool, force_tty: bool,
#[arg(long)]
force_stderr_tty: bool,
#[arg(long)]
pty_size: Option<TermSize>,
#[arg(short, long)] #[arg(short, long)]
env: Option<PathBuf>, env: Option<PathBuf>,
#[arg(short, long)] #[arg(short, long)]
@@ -181,6 +188,8 @@ pub fn launch(
_: ContainerCliContext, _: ContainerCliContext,
ExecParams { ExecParams {
force_tty, force_tty,
force_stderr_tty,
pty_size,
env, env,
workdir, workdir,
user, user,
@@ -188,34 +197,9 @@ pub fn launch(
command, command,
}: ExecParams, }: ExecParams,
) -> Result<(), Error> { ) -> Result<(), Error> {
kill_init(Path::new("/proc"), &chroot)?; use std::io::Write;
if (std::io::stdin().is_terminal()
&& std::io::stdout().is_terminal()
&& std::io::stderr().is_terminal())
|| force_tty
{
let mut cmd = TtySpawn::new("/usr/bin/start-cli");
cmd.arg("subcontainer").arg("launch-init");
if let Some(env) = env {
cmd.arg("--env").arg(env);
}
if let Some(workdir) = workdir {
cmd.arg("--workdir").arg(workdir);
}
if let Some(user) = user {
cmd.arg("--user").arg(user);
}
cmd.arg(&chroot);
cmd.args(command.iter());
nix::sched::unshare(CloneFlags::CLONE_NEWPID)
.with_ctx(|_| (ErrorKind::Filesystem, "unshare pid ns"))?;
nix::sched::unshare(CloneFlags::CLONE_NEWCGROUP)
.with_ctx(|_| (ErrorKind::Filesystem, "unshare cgroup ns"))?;
nix::sched::unshare(CloneFlags::CLONE_NEWIPC)
.with_ctx(|_| (ErrorKind::Filesystem, "unshare ipc ns"))?;
std::process::exit(cmd.spawn().with_kind(ErrorKind::Filesystem)?);
}
kill_init(Path::new("/proc"), &chroot)?;
let mut sig = signal_hook::iterator::Signals::new(FWD_SIGNALS)?; let mut sig = signal_hook::iterator::Signals::new(FWD_SIGNALS)?;
let (send_pid, recv_pid) = oneshot::channel(); let (send_pid, recv_pid) = oneshot::channel();
std::thread::spawn(move || { std::thread::spawn(move || {
@@ -229,75 +213,181 @@ pub fn launch(
} }
} }
}); });
let mut cmd = StdCommand::new("/usr/bin/start-cli");
cmd.arg("subcontainer").arg("launch-init"); let mut stdin = std::io::stdin();
if let Some(env) = env { let stdout = std::io::stdout();
cmd.arg("--env").arg(env); let stderr = std::io::stderr();
} let stderr_tty = force_stderr_tty || stderr.is_terminal();
if let Some(workdir) = workdir {
cmd.arg("--workdir").arg(workdir); let tty = force_tty || (stdin.is_terminal() && stdout.is_terminal());
}
if let Some(user) = user { let raw = if stdin.is_terminal() && stdout.is_terminal() {
cmd.arg("--user").arg(user); Some(termion::get_tty()?.into_raw_mode()?)
} } else {
cmd.arg(&chroot); None
cmd.args(&command); };
cmd.stdin(Stdio::piped());
cmd.stdout(Stdio::piped()); let pty_size = pty_size.or_else(|| TermSize::get_current());
cmd.stderr(Stdio::piped());
let (stdin_send, stdin_recv) = oneshot::channel(); let (stdin_send, stdin_recv) = oneshot::channel::<Box<dyn Write + Send>>();
std::thread::spawn(move || { std::thread::spawn(move || {
if let Ok(mut stdin) = stdin_recv.blocking_recv() { if let Ok(mut cstdin) = stdin_recv.blocking_recv() {
std::io::copy(&mut std::io::stdin(), &mut stdin).unwrap(); if tty {
let mut buf = [0_u8; CAP_1_KiB];
while let Ok(n) = stdin.read(&mut buf) {
if n == 0 {
break;
}
cstdin.write_all(&buf[..n]).ok();
cstdin.flush().ok();
}
} else {
std::io::copy(&mut stdin, &mut cstdin).unwrap();
}
} }
}); });
let (stdout_send, stdout_recv) = oneshot::channel(); let (stdout_send, stdout_recv) = oneshot::channel::<Box<dyn std::io::Read + Send>>();
std::thread::spawn(move || { let stdout_thread = std::thread::spawn(move || {
if let Ok(mut stdout) = stdout_recv.blocking_recv() { if let Ok(mut cstdout) = stdout_recv.blocking_recv() {
std::io::copy(&mut stdout, &mut std::io::stdout()).unwrap(); if tty {
} let mut stdout = stdout.lock();
}); let mut buf = [0_u8; CAP_1_KiB];
let (stderr_send, stderr_recv) = oneshot::channel(); while let Ok(n) = cstdout.read(&mut buf) {
std::thread::spawn(move || { if n == 0 {
if let Ok(mut stderr) = stderr_recv.blocking_recv() { break;
std::io::copy(&mut stderr, &mut std::io::stderr()).unwrap(); }
stdout.write_all(&buf[..n]).ok();
stdout.flush().ok();
}
} else {
std::io::copy(&mut cstdout, &mut stdout.lock()).unwrap();
}
} }
}); });
let (stderr_send, stderr_recv) = oneshot::channel::<Box<dyn std::io::Read + Send>>();
let stderr_thread = if !stderr_tty {
Some(std::thread::spawn(move || {
if let Ok(mut cstderr) = stderr_recv.blocking_recv() {
std::io::copy(&mut cstderr, &mut stderr.lock()).unwrap();
}
}))
} else {
None
};
nix::sched::unshare(CloneFlags::CLONE_NEWPID) nix::sched::unshare(CloneFlags::CLONE_NEWPID)
.with_ctx(|_| (ErrorKind::Filesystem, "unshare pid ns"))?; .with_ctx(|_| (ErrorKind::Filesystem, "unshare pid ns"))?;
nix::sched::unshare(CloneFlags::CLONE_NEWCGROUP) nix::sched::unshare(CloneFlags::CLONE_NEWCGROUP)
.with_ctx(|_| (ErrorKind::Filesystem, "unshare cgroup ns"))?; .with_ctx(|_| (ErrorKind::Filesystem, "unshare cgroup ns"))?;
nix::sched::unshare(CloneFlags::CLONE_NEWIPC) nix::sched::unshare(CloneFlags::CLONE_NEWIPC)
.with_ctx(|_| (ErrorKind::Filesystem, "unshare ipc ns"))?; .with_ctx(|_| (ErrorKind::Filesystem, "unshare ipc ns"))?;
let mut child = cmd
.spawn() if tty {
.map_err(color_eyre::eyre::Report::msg) use pty_process::blocking as pty_process;
.with_ctx(|_| (ErrorKind::Filesystem, "spawning child process"))?; let (pty, pts) = pty_process::open().with_kind(ErrorKind::Filesystem)?;
send_pid.send(child.id() as i32).unwrap_or_default(); let mut cmd = pty_process::Command::new("/usr/bin/start-cli");
stdin_send cmd = cmd.arg("subcontainer").arg("launch-init");
.send(child.stdin.take().unwrap()) if let Some(env) = env {
.unwrap_or_default(); cmd = cmd.arg("--env").arg(env);
stdout_send }
.send(child.stdout.take().unwrap()) if let Some(workdir) = workdir {
.unwrap_or_default(); cmd = cmd.arg("--workdir").arg(workdir);
stderr_send }
.send(child.stderr.take().unwrap()) if let Some(user) = user {
.unwrap_or_default(); cmd = cmd.arg("--user").arg(user);
// TODO: subreaping, signal handling }
let exit = child cmd = cmd.arg(&chroot).args(&command);
.wait() if !stderr_tty {
.with_ctx(|_| (ErrorKind::Filesystem, "waiting on child process"))?; cmd = cmd.stderr(Stdio::piped());
if let Some(code) = exit.code() { }
nix::mount::umount(&chroot.join("proc")) let mut child = cmd
.with_ctx(|_| (ErrorKind::Filesystem, "umount procfs"))?; .spawn(pts)
std::process::exit(code); .map_err(color_eyre::eyre::Report::msg)
} else if exit.success() { .with_ctx(|_| (ErrorKind::Filesystem, "spawning child process"))?;
Ok(()) send_pid.send(child.id() as i32).unwrap_or_default();
if let Some(pty_size) = pty_size {
let size = if let Some((x, y)) = pty_size.pixels {
::pty_process::Size::new_with_pixel(pty_size.size.0, pty_size.size.1, x, y)
} else {
::pty_process::Size::new(pty_size.size.0, pty_size.size.1)
};
pty.resize(size).with_kind(ErrorKind::Filesystem)?;
}
let shared = ArcPty(Arc::new(pty));
stdin_send
.send(Box::new(shared.clone()))
.unwrap_or_default();
stdout_send
.send(Box::new(shared.clone()))
.unwrap_or_default();
if let Some(stderr) = child.stderr.take() {
stderr_send.send(Box::new(stderr)).unwrap_or_default();
}
let exit = child
.wait()
.with_ctx(|_| (ErrorKind::Filesystem, "waiting on child process"))?;
stdout_thread.join().unwrap();
stderr_thread.map(|t| t.join().unwrap());
if let Some(code) = exit.code() {
drop(raw);
std::process::exit(code);
} else if exit.success() {
Ok(())
} else {
Err(Error::new(
color_eyre::eyre::Report::msg(exit),
ErrorKind::Unknown,
))
}
} else { } else {
Err(Error::new( let mut cmd = StdCommand::new("/usr/bin/start-cli");
color_eyre::eyre::Report::msg(exit), cmd.arg("subcontainer").arg("launch-init");
ErrorKind::Unknown, if let Some(env) = env {
)) cmd.arg("--env").arg(env);
}
if let Some(workdir) = workdir {
cmd.arg("--workdir").arg(workdir);
}
if let Some(user) = user {
cmd.arg("--user").arg(user);
}
cmd.arg(&chroot);
cmd.args(&command);
cmd.stdin(Stdio::piped());
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
let mut child = cmd
.spawn()
.map_err(color_eyre::eyre::Report::msg)
.with_ctx(|_| (ErrorKind::Filesystem, "spawning child process"))?;
send_pid.send(child.id() as i32).unwrap_or_default();
stdin_send
.send(Box::new(child.stdin.take().unwrap()))
.unwrap_or_default();
stdout_send
.send(Box::new(child.stdout.take().unwrap()))
.unwrap_or_default();
stderr_send
.send(Box::new(child.stderr.take().unwrap()))
.unwrap_or_default();
// TODO: subreaping, signal handling
let exit = child
.wait()
.with_ctx(|_| (ErrorKind::Filesystem, "waiting on child process"))?;
stdout_thread.join().unwrap();
stderr_thread.map(|t| t.join().unwrap());
if let Some(code) = exit.code() {
nix::mount::umount(&chroot.join("proc"))
.with_ctx(|_| (ErrorKind::Filesystem, "umount procfs"))?;
std::process::exit(code);
} else if exit.success() {
Ok(())
} else {
Err(Error::new(
color_eyre::eyre::Report::msg(exit),
ErrorKind::Unknown,
))
}
} }
} }
@@ -320,10 +410,28 @@ pub fn launch_init(_: ContainerCliContext, params: ExecParams) -> Result<(), Err
} }
} }
#[derive(Clone)]
struct ArcPty(Arc<pty_process::blocking::Pty>);
impl std::io::Write for ArcPty {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
(&*self.0).write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
(&*self.0).flush()
}
}
impl std::io::Read for ArcPty {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
(&*self.0).read(buf)
}
}
pub fn exec( pub fn exec(
_: ContainerCliContext, _: ContainerCliContext,
ExecParams { ExecParams {
force_tty, force_tty,
force_stderr_tty,
pty_size,
env, env,
workdir, workdir,
user, user,
@@ -331,41 +439,8 @@ pub fn exec(
command, command,
}: ExecParams, }: ExecParams,
) -> Result<(), Error> { ) -> Result<(), Error> {
if (std::io::stdin().is_terminal() use std::io::Write;
&& std::io::stdout().is_terminal()
&& std::io::stderr().is_terminal())
|| force_tty
{
let mut cmd = TtySpawn::new("/usr/bin/start-cli");
cmd.arg("subcontainer").arg("exec-command");
if let Some(env) = env {
cmd.arg("--env").arg(env);
}
if let Some(workdir) = workdir {
cmd.arg("--workdir").arg(workdir);
}
if let Some(user) = user {
cmd.arg("--user").arg(user);
}
cmd.arg(&chroot);
cmd.args(command.iter());
nix::sched::setns(
open_file_read(chroot.join("proc/1/ns/pid"))?,
CloneFlags::CLONE_NEWPID,
)
.with_ctx(|_| (ErrorKind::Filesystem, "set pid ns"))?;
nix::sched::setns(
open_file_read(chroot.join("proc/1/ns/cgroup"))?,
CloneFlags::CLONE_NEWCGROUP,
)
.with_ctx(|_| (ErrorKind::Filesystem, "set cgroup ns"))?;
nix::sched::setns(
open_file_read(chroot.join("proc/1/ns/ipc"))?,
CloneFlags::CLONE_NEWIPC,
)
.with_ctx(|_| (ErrorKind::Filesystem, "set ipc ns"))?;
std::process::exit(cmd.spawn().with_kind(ErrorKind::Filesystem)?);
}
let mut sig = signal_hook::iterator::Signals::new(FWD_SIGNALS)?; let mut sig = signal_hook::iterator::Signals::new(FWD_SIGNALS)?;
let (send_pid, recv_pid) = oneshot::channel(); let (send_pid, recv_pid) = oneshot::channel();
std::thread::spawn(move || { std::thread::spawn(move || {
@@ -379,40 +454,67 @@ pub fn exec(
} }
} }
}); });
let mut cmd = StdCommand::new("/usr/bin/start-cli");
cmd.arg("subcontainer").arg("exec-command"); let mut stdin = std::io::stdin();
if let Some(env) = env { let stdout = std::io::stdout();
cmd.arg("--env").arg(env); let stderr = std::io::stderr();
} let stderr_tty = force_stderr_tty || stderr.is_terminal();
if let Some(workdir) = workdir {
cmd.arg("--workdir").arg(workdir); let tty = force_tty || (stdin.is_terminal() && stdout.is_terminal());
}
if let Some(user) = user { let raw = if stdin.is_terminal() && stdout.is_terminal() {
cmd.arg("--user").arg(user); Some(termion::get_tty()?.into_raw_mode()?)
} } else {
cmd.arg(&chroot); None
cmd.args(&command); };
cmd.stdin(Stdio::piped());
cmd.stdout(Stdio::piped()); let pty_size = pty_size.or_else(|| TermSize::get_current());
cmd.stderr(Stdio::piped());
let (stdin_send, stdin_recv) = oneshot::channel(); let (stdin_send, stdin_recv) = oneshot::channel::<Box<dyn Write + Send>>();
std::thread::spawn(move || { std::thread::spawn(move || {
if let Ok(mut stdin) = stdin_recv.blocking_recv() { if let Ok(mut cstdin) = stdin_recv.blocking_recv() {
std::io::copy(&mut std::io::stdin(), &mut stdin).unwrap(); if tty {
let mut buf = [0_u8; CAP_1_KiB];
while let Ok(n) = stdin.read(&mut buf) {
if n == 0 {
break;
}
cstdin.write_all(&buf[..n]).ok();
cstdin.flush().ok();
}
} else {
std::io::copy(&mut stdin, &mut cstdin).unwrap();
}
} }
}); });
let (stdout_send, stdout_recv) = oneshot::channel(); let (stdout_send, stdout_recv) = oneshot::channel::<Box<dyn std::io::Read + Send>>();
std::thread::spawn(move || { let stdout_thread = std::thread::spawn(move || {
if let Ok(mut stdout) = stdout_recv.blocking_recv() { if let Ok(mut cstdout) = stdout_recv.blocking_recv() {
std::io::copy(&mut stdout, &mut std::io::stdout()).unwrap(); if tty {
} let mut stdout = stdout.lock();
}); let mut buf = [0_u8; CAP_1_KiB];
let (stderr_send, stderr_recv) = oneshot::channel(); while let Ok(n) = cstdout.read(&mut buf) {
std::thread::spawn(move || { if n == 0 {
if let Ok(mut stderr) = stderr_recv.blocking_recv() { break;
std::io::copy(&mut stderr, &mut std::io::stderr()).unwrap(); }
stdout.write_all(&buf[..n]).ok();
stdout.flush().ok();
}
} else {
std::io::copy(&mut cstdout, &mut stdout.lock()).unwrap();
}
} }
}); });
let (stderr_send, stderr_recv) = oneshot::channel::<Box<dyn std::io::Read + Send>>();
let stderr_thread = if !stderr_tty {
Some(std::thread::spawn(move || {
if let Ok(mut cstderr) = stderr_recv.blocking_recv() {
std::io::copy(&mut cstderr, &mut stderr.lock()).unwrap();
}
}))
} else {
None
};
nix::sched::setns( nix::sched::setns(
open_file_read(chroot.join("proc/1/ns/pid"))?, open_file_read(chroot.join("proc/1/ns/pid"))?,
CloneFlags::CLONE_NEWPID, CloneFlags::CLONE_NEWPID,
@@ -428,32 +530,110 @@ pub fn exec(
CloneFlags::CLONE_NEWIPC, CloneFlags::CLONE_NEWIPC,
) )
.with_ctx(|_| (ErrorKind::Filesystem, "set ipc ns"))?; .with_ctx(|_| (ErrorKind::Filesystem, "set ipc ns"))?;
let mut child = cmd
.spawn() if tty {
.map_err(color_eyre::eyre::Report::msg) use pty_process::blocking as pty_process;
.with_ctx(|_| (ErrorKind::Filesystem, "spawning child process"))?; let (pty, pts) = pty_process::open().with_kind(ErrorKind::Filesystem)?;
send_pid.send(child.id() as i32).unwrap_or_default(); let mut cmd = pty_process::Command::new("/usr/bin/start-cli");
stdin_send cmd = cmd.arg("subcontainer").arg("exec-command");
.send(child.stdin.take().unwrap()) if let Some(env) = env {
.unwrap_or_default(); cmd = cmd.arg("--env").arg(env);
stdout_send }
.send(child.stdout.take().unwrap()) if let Some(workdir) = workdir {
.unwrap_or_default(); cmd = cmd.arg("--workdir").arg(workdir);
stderr_send }
.send(child.stderr.take().unwrap()) if let Some(user) = user {
.unwrap_or_default(); cmd = cmd.arg("--user").arg(user);
let exit = child }
.wait() cmd = cmd.arg(&chroot).args(&command);
.with_ctx(|_| (ErrorKind::Filesystem, "waiting on child process"))?; if !stderr_tty {
if let Some(code) = exit.code() { cmd = cmd.stderr(Stdio::piped());
std::process::exit(code); }
} else if exit.success() { let mut child = cmd
Ok(()) .spawn(pts)
.map_err(color_eyre::eyre::Report::msg)
.with_ctx(|_| (ErrorKind::Filesystem, "spawning child process"))?;
send_pid.send(child.id() as i32).unwrap_or_default();
if let Some(pty_size) = pty_size {
let size = if let Some((x, y)) = pty_size.pixels {
::pty_process::Size::new_with_pixel(pty_size.size.0, pty_size.size.1, x, y)
} else {
::pty_process::Size::new(pty_size.size.0, pty_size.size.1)
};
pty.resize(size).with_kind(ErrorKind::Filesystem)?;
}
let shared = ArcPty(Arc::new(pty));
stdin_send
.send(Box::new(shared.clone()))
.unwrap_or_default();
stdout_send
.send(Box::new(shared.clone()))
.unwrap_or_default();
if let Some(stderr) = child.stderr.take() {
stderr_send.send(Box::new(stderr)).unwrap_or_default();
}
let exit = child
.wait()
.with_ctx(|_| (ErrorKind::Filesystem, "waiting on child process"))?;
stdout_thread.join().unwrap();
stderr_thread.map(|t| t.join().unwrap());
if let Some(code) = exit.code() {
drop(raw);
std::process::exit(code);
} else if exit.success() {
Ok(())
} else {
Err(Error::new(
color_eyre::eyre::Report::msg(exit),
ErrorKind::Unknown,
))
}
} else { } else {
Err(Error::new( let mut cmd = StdCommand::new("/usr/bin/start-cli");
color_eyre::eyre::Report::msg(exit), cmd.arg("subcontainer").arg("exec-command");
ErrorKind::Unknown, if let Some(env) = env {
)) cmd.arg("--env").arg(env);
}
if let Some(workdir) = workdir {
cmd.arg("--workdir").arg(workdir);
}
if let Some(user) = user {
cmd.arg("--user").arg(user);
}
cmd.arg(&chroot);
cmd.args(&command);
cmd.stdin(Stdio::piped());
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
let mut child = cmd
.spawn()
.map_err(color_eyre::eyre::Report::msg)
.with_ctx(|_| (ErrorKind::Filesystem, "spawning child process"))?;
send_pid.send(child.id() as i32).unwrap_or_default();
stdin_send
.send(Box::new(child.stdin.take().unwrap()))
.unwrap_or_default();
stdout_send
.send(Box::new(child.stdout.take().unwrap()))
.unwrap_or_default();
stderr_send
.send(Box::new(child.stderr.take().unwrap()))
.unwrap_or_default();
let exit = child
.wait()
.with_ctx(|_| (ErrorKind::Filesystem, "waiting on child process"))?;
stdout_thread.join().unwrap();
stderr_thread.map(|t| t.join().unwrap());
if let Some(code) = exit.code() {
std::process::exit(code);
} else if exit.success() {
Ok(())
} else {
Err(Error::new(
color_eyre::eyre::Report::msg(exit),
ErrorKind::Unknown,
))
}
} }
} }

View File

@@ -13,7 +13,8 @@ use chrono::{DateTime, Utc};
use clap::Parser; use clap::Parser;
use futures::future::BoxFuture; use futures::future::BoxFuture;
use futures::stream::FusedStream; use futures::stream::FusedStream;
use futures::{SinkExt, StreamExt, TryStreamExt}; use futures::{FutureExt, SinkExt, StreamExt, TryStreamExt};
use helpers::NonDetachingJoinHandle;
use imbl_value::{json, InternedString}; use imbl_value::{json, InternedString};
use itertools::Itertools; use itertools::Itertools;
use models::{ActionId, HostId, ImageId, PackageId, ProcedureName}; use models::{ActionId, HostId, ImageId, PackageId, ProcedureName};
@@ -23,6 +24,7 @@ use rpc_toolkit::{from_fn_async, CallRemoteHandler, Empty, HandlerArgs, HandlerF
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use service_actor::ServiceActor; use service_actor::ServiceActor;
use start_stop::StartStop; use start_stop::StartStop;
use termion::raw::IntoRawMode;
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::process::Command; use tokio::process::Command;
use tokio::sync::Notify; use tokio::sync::Notify;
@@ -43,7 +45,7 @@ use crate::s9pk::S9pk;
use crate::service::action::update_requested_actions; use crate::service::action::update_requested_actions;
use crate::service::service_map::InstallProgressHandles; use crate::service::service_map::InstallProgressHandles;
use crate::util::actor::concurrent::ConcurrentActor; use crate::util::actor::concurrent::ConcurrentActor;
use crate::util::io::{create_file, AsyncReadStream}; use crate::util::io::{create_file, AsyncReadStream, TermSize};
use crate::util::net::WebSocketExt; use crate::util::net::WebSocketExt;
use crate::util::serde::{NoOutput, Pem}; use crate::util::serde::{NoOutput, Pem};
use crate::util::Never; use crate::util::Never;
@@ -739,6 +741,7 @@ pub struct AttachParams {
#[ts(type = "string[]")] #[ts(type = "string[]")]
pub command: Vec<OsString>, pub command: Vec<OsString>,
pub tty: bool, pub tty: bool,
pub stderr_tty: bool,
#[ts(skip)] #[ts(skip)]
#[serde(rename = "__auth_session")] #[serde(rename = "__auth_session")]
session: Option<InternedString>, session: Option<InternedString>,
@@ -755,6 +758,7 @@ pub async fn attach(
id, id,
command, command,
tty, tty,
stderr_tty,
session, session,
subcontainer, subcontainer,
image_id, image_id,
@@ -864,6 +868,7 @@ pub async fn attach(
subcontainer_id: Guid, subcontainer_id: Guid,
command: Vec<OsString>, command: Vec<OsString>,
tty: bool, tty: bool,
stderr_tty: bool,
image_id: ImageId, image_id: ImageId,
workdir: Option<String>, workdir: Option<String>,
root_command: &RootCommand, root_command: &RootCommand,
@@ -896,6 +901,10 @@ pub async fn attach(
cmd.arg("--force-tty"); cmd.arg("--force-tty");
} }
if stderr_tty {
cmd.arg("--force-stderr-tty");
}
cmd.arg(&root_path).arg("--"); cmd.arg(&root_path).arg("--");
if command.is_empty() { if command.is_empty() {
@@ -912,7 +921,7 @@ pub async fn attach(
let pid = nix::unistd::Pid::from_raw(child.id().or_not_found("child pid")? as i32); let pid = nix::unistd::Pid::from_raw(child.id().or_not_found("child pid")? as i32);
let mut stdin = child.stdin.take().or_not_found("child stdin")?; let mut stdin = Some(child.stdin.take().or_not_found("child stdin")?);
let mut current_in = "stdin".to_owned(); let mut current_in = "stdin".to_owned();
let mut current_out = "stdout"; let mut current_out = "stdout";
@@ -943,6 +952,10 @@ pub async fn attach(
ws.send(Message::Binary(out)) ws.send(Message::Binary(out))
.await .await
.with_kind(ErrorKind::Network)?; .with_kind(ErrorKind::Network)?;
} else {
ws.send(Message::Text("close-stdout".into()))
.await
.with_kind(ErrorKind::Network)?;
} }
} }
err = stderr.try_next() => { err = stderr.try_next() => {
@@ -956,6 +969,10 @@ pub async fn attach(
ws.send(Message::Binary(err)) ws.send(Message::Binary(err))
.await .await
.with_kind(ErrorKind::Network)?; .with_kind(ErrorKind::Network)?;
} else {
ws.send(Message::Text("close-stderr".into()))
.await
.with_kind(ErrorKind::Network)?;
} }
} }
msg = ws.try_next() => { msg = ws.try_next() => {
@@ -967,7 +984,12 @@ pub async fn attach(
Message::Binary(data) => { Message::Binary(data) => {
match &*current_in { match &*current_in {
"stdin" => { "stdin" => {
stdin.write_all(&data).await?; if let Some(stdin) = &mut stdin {
stdin.write_all(&data).await?;
}
}
"close-stdin" => {
stdin.take();
} }
"signal" => { "signal" => {
if data.len() != 4 { if data.len() != 4 {
@@ -1022,6 +1044,7 @@ pub async fn attach(
subcontainer_id, subcontainer_id,
command, command,
tty, tty,
stderr_tty,
image_id, image_id,
workdir, workdir,
&root_command, &root_command,
@@ -1100,6 +1123,7 @@ pub struct CliAttachParams {
#[arg(long, short)] #[arg(long, short)]
image_id: Option<ImageId>, image_id: Option<ImageId>,
} }
#[instrument[skip_all]]
pub async fn cli_attach( pub async fn cli_attach(
HandlerArgs { HandlerArgs {
context, context,
@@ -1109,8 +1133,39 @@ pub async fn cli_attach(
.. ..
}: HandlerArgs<CliContext, CliAttachParams>, }: HandlerArgs<CliContext, CliAttachParams>,
) -> Result<(), Error> { ) -> Result<(), Error> {
use std::io::Write;
use tokio_tungstenite::tungstenite::Message; use tokio_tungstenite::tungstenite::Message;
let stdin = std::io::stdin();
let stdout = std::io::stdout();
let stderr = std::io::stderr();
let tty = params.force_tty || (stdin.is_terminal() && stdout.is_terminal());
let raw = if stdin.is_terminal() && stdout.is_terminal() {
Some(termion::get_tty()?.into_raw_mode()?)
} else {
None
};
let (kill, thread_kill) = tokio::sync::oneshot::channel();
let (thread_send, recv) = tokio::sync::mpsc::channel(4 * CAP_1_KiB);
let stdin_thread: NonDetachingJoinHandle<()> = tokio::task::spawn_blocking(move || {
use std::io::Read;
let mut stdin = stdin.lock().bytes();
while thread_kill.is_empty() {
if let Some(b) = stdin.next() {
thread_send.blocking_send(b).unwrap();
} else {
break;
}
}
})
.into();
let mut stdin = Some(recv);
let guid: Guid = from_value( let guid: Guid = from_value(
context context
.call_remote::<RpcContext>( .call_remote::<RpcContext>(
@@ -1118,10 +1173,9 @@ pub async fn cli_attach(
json!({ json!({
"id": params.id, "id": params.id,
"command": params.command, "command": params.command,
"tty": (std::io::stdin().is_terminal() "tty": tty,
&& std::io::stdout().is_terminal() "stderrTty": stderr.is_terminal(),
&& std::io::stderr().is_terminal()) "ptySize": if tty { TermSize::get_current() } else { None },
|| params.force_tty,
"subcontainer": params.subcontainer, "subcontainer": params.subcontainer,
"imageId": params.image_id, "imageId": params.image_id,
"name": params.name, "name": params.name,
@@ -1136,9 +1190,8 @@ pub async fn cli_attach(
ws.send(Message::Text(current_in.into())) ws.send(Message::Text(current_in.into()))
.await .await
.with_kind(ErrorKind::Network)?; .with_kind(ErrorKind::Network)?;
let mut stdin = AsyncReadStream::new(tokio::io::stdin(), 4 * CAP_1_KiB).fuse(); let mut stdout = Some(stdout);
let mut stdout = tokio::io::stdout(); let mut stderr = Some(stderr);
let mut stderr = tokio::io::stderr();
loop { loop {
futures::select_biased! { futures::select_biased! {
// signal = tokio:: => { // signal = tokio:: => {
@@ -1153,8 +1206,15 @@ pub async fn cli_attach(
// i32::to_be_bytes(exit.into_raw()).to_vec() // i32::to_be_bytes(exit.into_raw()).to_vec()
// )).await.with_kind(ErrorKind::Network)?; // )).await.with_kind(ErrorKind::Network)?;
// } // }
input = stdin.try_next() => { input = stdin.as_mut().map_or(
if let Some(input) = input? { futures::future::Either::Left(futures::future::pending()),
|s| futures::future::Either::Right(s.recv())
).fuse() => {
if let (Some(input), Some(stdin)) = (input.transpose()?, &mut stdin) {
let mut input = vec![input];
while let Ok(b) = stdin.try_recv() {
input.push(b?);
}
if current_in != "stdin" { if current_in != "stdin" {
ws.send(Message::Text("stdin".into())) ws.send(Message::Text("stdin".into()))
.await .await
@@ -1164,6 +1224,11 @@ pub async fn cli_attach(
ws.send(Message::Binary(input)) ws.send(Message::Binary(input))
.await .await
.with_kind(ErrorKind::Network)?; .with_kind(ErrorKind::Network)?;
} else {
ws.send(Message::Text("close-stdin".into()))
.await
.with_kind(ErrorKind::Network)?;
stdin.take();
} }
} }
msg = ws.try_next() => { msg = ws.try_next() => {
@@ -1175,12 +1240,22 @@ pub async fn cli_attach(
Message::Binary(data) => { Message::Binary(data) => {
match &*current_out { match &*current_out {
"stdout" => { "stdout" => {
stdout.write_all(&data).await?; if let Some(stdout) = &mut stdout {
stdout.flush().await?; stdout.write_all(&data)?;
stdout.flush()?;
}
} }
"stderr" => { "stderr" => {
stderr.write_all(&data).await?; if let Some(stderr) = &mut stderr {
stderr.flush().await?; stderr.write_all(&data)?;
stderr.flush()?;
}
}
"close-stdout" => {
stdout.take();
}
"close-stderr" => {
stderr.take();
} }
"exit" => { "exit" => {
if data.len() != 4 { if data.len() != 4 {
@@ -1192,6 +1267,7 @@ pub async fn cli_attach(
let mut exit_buf = [0u8; 4]; let mut exit_buf = [0u8; 4];
exit_buf.clone_from_slice(&data); exit_buf.clone_from_slice(&data);
let code = i32::from_be_bytes(exit_buf); let code = i32::from_be_bytes(exit_buf);
drop(raw);
std::process::exit(code); std::process::exit(code);
} }
_ => (), _ => (),
@@ -1208,6 +1284,8 @@ pub async fn cli_attach(
_ => () _ => ()
} }
} else { } else {
kill.send(()).ok();
stdin_thread.wait_for_abort().await.log_err();
return Ok(()) return Ok(())
} }
} }

View File

@@ -5,16 +5,20 @@ use std::mem::MaybeUninit;
use std::os::unix::prelude::MetadataExt; use std::os::unix::prelude::MetadataExt;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::pin::Pin; use std::pin::Pin;
use std::str::FromStr;
use std::sync::atomic::AtomicU64; use std::sync::atomic::AtomicU64;
use std::sync::Arc; use std::sync::Arc;
use std::task::{Poll, Waker}; use std::task::{Poll, Waker};
use std::time::Duration; use std::time::Duration;
use bytes::{Buf, BytesMut}; use bytes::{Buf, BytesMut};
use clap::builder::ValueParserFactory;
use futures::future::{BoxFuture, Fuse}; use futures::future::{BoxFuture, Fuse};
use futures::{AsyncSeek, FutureExt, Stream, TryStreamExt}; use futures::{AsyncSeek, FutureExt, Stream, TryStreamExt};
use helpers::NonDetachingJoinHandle; use helpers::NonDetachingJoinHandle;
use models::FromStrParser;
use nix::unistd::{Gid, Uid}; use nix::unistd::{Gid, Uid};
use serde::{Deserialize, Serialize};
use tokio::fs::{File, OpenOptions}; use tokio::fs::{File, OpenOptions};
use tokio::io::{ use tokio::io::{
duplex, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, DuplexStream, ReadBuf, WriteHalf, duplex, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, DuplexStream, ReadBuf, WriteHalf,
@@ -24,6 +28,7 @@ use tokio::sync::{Notify, OwnedMutexGuard};
use tokio::time::{Instant, Sleep}; use tokio::time::{Instant, Sleep};
use crate::prelude::*; use crate::prelude::*;
use crate::util::sync::SyncMutex;
use crate::{CAP_1_KiB, CAP_1_MiB}; use crate::{CAP_1_KiB, CAP_1_MiB};
pub trait AsyncReadSeek: AsyncRead + AsyncSeek {} pub trait AsyncReadSeek: AsyncRead + AsyncSeek {}
@@ -1395,3 +1400,73 @@ impl<T: AsyncRead> Stream for AsyncReadStream<T> {
} }
} }
} }
pub struct SharedIO<T>(pub Arc<SyncMutex<T>>);
impl<T> SharedIO<T> {
pub fn new(t: T) -> Self {
Self(Arc::new(SyncMutex::new(t)))
}
}
impl<T> Clone for SharedIO<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T: std::io::Write> std::io::Write for SharedIO<T> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.mutate(|w| w.write(buf))
}
fn flush(&mut self) -> std::io::Result<()> {
self.0.mutate(|w| w.flush())
}
}
impl<T: std::io::Read> std::io::Read for SharedIO<T> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.0.mutate(|r| r.read(buf))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TermSize {
pub size: (u16, u16),
pub pixels: Option<(u16, u16)>,
}
impl TermSize {
pub fn get_current() -> Option<Self> {
if let Some(size) = termion::terminal_size().ok() {
Some(Self {
size,
pixels: termion::terminal_size_pixels().ok(),
})
} else {
None
}
}
}
impl FromStr for TermSize {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
(|| {
let mut split = s.split(":");
let row: u16 = split.next()?.parse().ok()?;
let col: u16 = split.next()?.parse().ok()?;
let size = (row, col);
let pixels = if let Some(x) = split.next() {
let x: u16 = x.parse().ok()?;
let y: u16 = split.next()?.parse().ok()?;
Some((x, y))
} else {
None
};
Some(Self { size, pixels }).filter(|_| split.next().is_none())
})()
.ok_or_else(|| Error::new(eyre!("invalid pty size"), ErrorKind::ParseNumber))
}
}
impl ValueParserFactory for TermSize {
type Parser = FromStrParser<Self>;
fn value_parser() -> Self::Parser {
FromStrParser::new()
}
}