//! Dev container transport of remote development: the Dev Containers CLI (`devcontainer up`) //! builds or starts the container of a local folder, then `openidea-server --stdio` runs in it //! through `docker exec`, installed there the way it is in a WSL distribution. //! //! The container id, user and folder come from the CLI's output, are checked, and only ever //! reach `docker exec` as separate arguments; only fixed scripts reach `sh -c`. //! //! A forwarded port is a local listener whose every connection runs //! `openidea-server --connect localhost ` in the container and pipes through it: the //! container's own network is reached the same way on every Docker setup. use crate::remote_dev::{Progress, RemoteStage, SERVER_BIN}; use crate::remote_wsl::{Probe, Tail, install_script, parse_probe, probe_script, upload, version_dir}; use ide_core::{IdeError, IdeResult}; use ide_jsonrpc::{Handler, Peer}; use ide_remote::container_tunnel::Exec; use ide_remote::devcontainer::{Up, parse_up}; use std::path::{Path, PathBuf}; use std::process::Stdio; use std::sync::Arc; use std::time::Duration; use tokio::io::AsyncBufReadExt; /// Building an image (and its features) can take a while on a first open. const UP_TIMEOUT: Duration = Duration::from_secs(30 * 60); const PROBE_TIMEOUT: Duration = Duration::from_secs(60); /// The Dev Containers CLI: the IDE-managed copy (Settings › Tools) or one on `PATH`. fn cli(data_dir: &Path) -> IdeResult { ide_tools::Tools::new(data_dir) .info("devcontainer", &[]) .ok() .and_then(|t| t.path) .map(ide_core::IpcPath::into_path_buf) .ok_or_else(|| { IdeError::unavailable( "The Dev Containers CLI is not installed. Install it in Settings › Tools › Dev Containers CLI.", ) }) } /// `devcontainer up` for `folder` (with configuration `config`, relative to it; empty = the /// default one). Its log goes to `tail` and the IDE log. async fn up(cli: &Path, docker: &Path, folder: &Path, config: &str, tail: &Tail) -> IdeResult { let mut cmd = tokio::process::Command::new(cli); ide_core::process::prepare(cmd.as_std_mut()); cmd.arg("up").arg("--workspace-folder").arg(folder); if !config.trim().is_empty() { cmd.arg("--config").arg(folder.join(config.trim())); } cmd.arg("--docker-path") .arg(docker) .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .kill_on_drop(true); let mut child = cmd.spawn().map_err(|e| IdeError::unavailable(format!("cannot start devcontainer: {e}")))?; let stderr = child.stderr.take(); let log = tail.clone(); let reader = tokio::spawn(async move { let Some(stderr) = stderr else { return }; let mut lines = tokio::io::BufReader::new(stderr).lines(); while let Ok(Some(line)) = lines.next_line().await { tracing::info!(target: "devcontainer", "{line}"); log.push(line); } }); let out = tokio::time::timeout(UP_TIMEOUT, child.wait_with_output()) .await .map_err(|_| IdeError::unavailable("devcontainer up did not finish in time"))? .map_err(|e| IdeError::unavailable(format!("devcontainer up failed: {e}")))?; let _ = reader.await; let result = parse_up(&String::from_utf8_lossy(&out.stdout)); if out.status.success() { return result; } let error = result .err() .unwrap_or_else(|| IdeError::unavailable(format!("devcontainer up exited with {status}", status = out.status))); Err(match tail.text() { t if t.is_empty() => error, t => IdeError::unavailable(format!("{}\n{t}", error.message)), }) } /// `docker exec [-i] -u sh -c