From a218f1a911229cbd1cd65b5e6b88e8c7c7432ec9 Mon Sep 17 00:00:00 2001 From: wsp Date: Mon, 27 Jul 2026 18:08:11 +0800 Subject: [PATCH 1/2] fix(memory): replace workspace git CLI with libgit2 - remove the runtime dependency on the system Git executable - build memory baselines and diffs through vendored libgit2 - preserve ignored ad-hoc notes in Phase 2 workspace diffs - add regression coverage for ignored memory notes --- .../core/src/agentic/memories/workspace.rs | 3 - src/crates/services/services-core/Cargo.toml | 5 + .../src/session/memory_workspace.rs | 349 ++++++++---------- 3 files changed, 157 insertions(+), 200 deletions(-) diff --git a/src/crates/assembly/core/src/agentic/memories/workspace.rs b/src/crates/assembly/core/src/agentic/memories/workspace.rs index 01d3aab055..0444505f10 100644 --- a/src/crates/assembly/core/src/agentic/memories/workspace.rs +++ b/src/crates/assembly/core/src/agentic/memories/workspace.rs @@ -340,9 +340,6 @@ fn map_memory_workspace_git_error( MemoryWorkspaceGitError::Join { source } => { BitFunError::service(format!("{join_context}: {source}")) } - error @ MemoryWorkspaceGitError::UnsupportedGitStatus { .. } => { - BitFunError::service(error.to_string()) - } error => BitFunError::io(error.to_string()), } } diff --git a/src/crates/services/services-core/Cargo.toml b/src/crates/services/services-core/Cargo.toml index f0fbba31c3..ceb4415820 100644 --- a/src/crates/services/services-core/Cargo.toml +++ b/src/crates/services/services-core/Cargo.toml @@ -21,6 +21,7 @@ serde_json = { workspace = true } serde_yaml = { workspace = true, optional = true } base64 = { workspace = true } chrono = { workspace = true } +git2 = { workspace = true } dunce = { workspace = true, optional = true } zip = { workspace = true, optional = true } thiserror = { workspace = true } @@ -43,6 +44,10 @@ windows = { workspace = true, features = [ "Win32_System_Threading", ] } +# Keep libgit2 self-contained on Unix, matching the product assembly dependency. +[target.'cfg(not(windows))'.dependencies] +git2 = { workspace = true, features = ["vendored-openssl"] } + [target.'cfg(unix)'.dependencies] libc = { workspace = true } diff --git a/src/crates/services/services-core/src/session/memory_workspace.rs b/src/crates/services/services-core/src/session/memory_workspace.rs index 7864ab65fe..960704f354 100644 --- a/src/crates/services/services-core/src/session/memory_workspace.rs +++ b/src/crates/services/services-core/src/session/memory_workspace.rs @@ -1,6 +1,5 @@ -use std::collections::BTreeSet; +use git2::{Delta, Diff, DiffFormat, DiffOptions, IndexAddOption, Repository, Signature}; use std::path::{Path, PathBuf}; -use std::process::Command; use thiserror::Error; const MEMORY_BASELINE_COMMIT_MESSAGE: &str = "Memory workspace baseline"; @@ -26,52 +25,24 @@ pub enum MemoryWorkspaceGitError { #[source] source: std::io::Error, }, - #[error("Failed to read memory workspace directory {path}: {source}")] - ReadDirectory { - path: PathBuf, - #[source] - source: std::io::Error, - }, - #[error("Failed to read memory workspace directory entry {path}: {source}")] - ReadDirectoryEntry { - path: PathBuf, - #[source] - source: std::io::Error, - }, - #[error("Failed to inspect memory workspace entry {path}: {source}")] - InspectEntry { - path: PathBuf, - #[source] - source: std::io::Error, - }, - #[error("Failed to normalize memory workspace path {path} under {root}: {source}")] - NormalizePath { - root: PathBuf, - path: PathBuf, - #[source] - source: std::path::StripPrefixError, - }, #[error("Failed to read added memory workspace file {path}: {source}")] ReadAddedFile { path: PathBuf, #[source] source: std::io::Error, }, - #[error("Failed to run memory workspace git command in {root}: {source}")] - RunGit { + #[error("Memory workspace git operation {operation} failed in {root}: {source}")] + GitOperation { root: PathBuf, + operation: &'static str, #[source] - source: std::io::Error, + source: git2::Error, }, - #[error("Memory workspace git command failed in {root}: {stderr}")] - GitFailed { root: PathBuf, stderr: String }, - #[error("Memory workspace git output was not UTF-8: {source}")] - GitOutputUtf8 { + #[error("Memory workspace diff output was not UTF-8: {source}")] + DiffOutputUtf8 { #[source] source: std::string::FromUtf8Error, }, - #[error("Unsupported memory workspace git status '{status}': {line}")] - UnsupportedGitStatus { status: char, line: String }, #[error("Memory workspace git task failed: {source}")] Join { #[source] @@ -121,9 +92,12 @@ pub async fn ensure_memory_workspace_git_baseline( tokio::task::spawn_blocking(move || { create_root(&root)?; - if root.join(".git").is_dir() && run_git(&root, &["rev-parse", "--verify", "HEAD"]).is_ok() - { - return Ok(()); + if root.join(".git").is_dir() { + if let Ok(repository) = Repository::open(&root) { + if repository.head().is_ok() { + return Ok(()); + } + } } reset_memory_workspace_git_baseline_sync(&root) @@ -181,17 +155,35 @@ fn create_root(root: &Path) -> Result<(), MemoryWorkspaceGitError> { fn reset_memory_workspace_git_baseline_sync(root: &Path) -> Result<(), MemoryWorkspaceGitError> { create_root(root)?; remove_git_metadata(root)?; - run_git_raw(root, &["init"])?; - run_git(root, &["add", "-A"])?; - run_git( - root, - &[ - "commit", - "--allow-empty", - "-m", + let repository = Repository::init(root) + .map_err(|source| git_operation(root, "initialize repository", source))?; + let mut index = repository + .index() + .map_err(|source| git_operation(root, "open repository index", source))?; + index + .add_all(["*"].iter(), IndexAddOption::DEFAULT, None) + .map_err(|source| git_operation(root, "stage workspace files", source))?; + index + .write() + .map_err(|source| git_operation(root, "write repository index", source))?; + let tree_id = index + .write_tree() + .map_err(|source| git_operation(root, "write baseline tree", source))?; + let tree = repository + .find_tree(tree_id) + .map_err(|source| git_operation(root, "load baseline tree", source))?; + let signature = Signature::now("BitFun", "bitfun@localhost") + .map_err(|source| git_operation(root, "create baseline signature", source))?; + repository + .commit( + Some("HEAD"), + &signature, + &signature, MEMORY_BASELINE_COMMIT_MESSAGE, - ], - )?; + &tree, + &[], + ) + .map_err(|source| git_operation(root, "commit baseline", source))?; Ok(()) } @@ -215,36 +207,32 @@ fn remove_git_metadata(root: &Path) -> Result<(), MemoryWorkspaceGitError> { } fn memory_workspace_diff_sync(root: &Path) -> Result { - run_git(root, &["rev-parse", "--verify", "HEAD"])?; - - let tracked_status = git_stdout( - root, - &["diff", "--name-status", "--no-renames", "HEAD", "--"], - )?; - let mut changes = parse_git_name_status(&tracked_status)?; - let head_paths = git_z_stdout(root, &["ls-tree", "-r", "--name-only", "-z", "HEAD"])?; - let head_paths = parse_nul_paths(&head_paths); - let current_paths = collect_current_memory_paths(root)?; - - for path in current_paths.difference(&head_paths) { - changes.push(MemoryWorkspaceChange { - status: MemoryWorkspaceChangeStatus::Added, - path: path.clone(), - }); - } - - changes.sort_by(|left, right| left.path.cmp(&right.path)); - changes.dedup_by(|left, right| left.path == right.path); - - let mut unified_diff = git_stdout( - root, - &["diff", "--no-ext-diff", "--no-renames", "HEAD", "--"], - )?; - for change in changes - .iter() - .filter(|change| change.status == MemoryWorkspaceChangeStatus::Added) - { - unified_diff.push_str(&render_added_file_diff(root, &change.path)?); + let repository = + Repository::open(root).map_err(|source| git_operation(root, "open repository", source))?; + let head = repository + .head() + .map_err(|source| git_operation(root, "resolve baseline HEAD", source))?; + let baseline = head + .peel_to_tree() + .map_err(|source| git_operation(root, "load baseline tree", source))?; + let mut options = DiffOptions::new(); + options + .include_untracked(true) + .recurse_untracked_dirs(true) + .include_ignored(true) + .recurse_ignored_dirs(true) + .show_untracked_content(true) + .include_typechange(true); + let diff = repository + .diff_tree_to_workdir_with_index(Some(&baseline), Some(&mut options)) + .map_err(|source| git_operation(root, "compute workspace diff", source))?; + let changes = collect_workspace_changes(&diff); + let mut unified_diff = render_workspace_diff(&diff, root)?; + for path in ignored_workspace_paths(&diff) { + // libgit2 reports ignored entries but does not render their content as a + // patch, while the previous filesystem scan included every file except + // `.git`. Append an equivalent added-file patch to preserve that input. + unified_diff.push_str(&render_added_file_diff(root, &path)?); } Ok(MemoryWorkspaceDiff { @@ -253,94 +241,50 @@ fn memory_workspace_diff_sync(root: &Path) -> Result Result, MemoryWorkspaceGitError> { - let mut paths = BTreeSet::new(); - collect_current_memory_paths_inner(root, root, &mut paths)?; - Ok(paths) -} - -fn collect_current_memory_paths_inner( - root: &Path, - dir: &Path, - paths: &mut BTreeSet, -) -> Result<(), MemoryWorkspaceGitError> { - for entry in - std::fs::read_dir(dir).map_err(|source| MemoryWorkspaceGitError::ReadDirectory { - path: dir.to_path_buf(), - source, - })? - { - let entry = entry.map_err(|source| MemoryWorkspaceGitError::ReadDirectoryEntry { - path: dir.to_path_buf(), - source, - })?; - let path = entry.path(); - if path.file_name().and_then(|name| name.to_str()) == Some(".git") { - continue; - } - let metadata = - entry - .metadata() - .map_err(|source| MemoryWorkspaceGitError::InspectEntry { - path: path.clone(), - source, - })?; - if metadata.is_dir() { - collect_current_memory_paths_inner(root, &path, paths)?; - } else if metadata.is_file() { - paths.insert(relative_memory_path(root, &path)?); - } - } - Ok(()) -} - -fn relative_memory_path(root: &Path, path: &Path) -> Result { - let relative = - path.strip_prefix(root) - .map_err(|source| MemoryWorkspaceGitError::NormalizePath { - root: root.to_path_buf(), - path: path.to_path_buf(), - source, - })?; - Ok(relative.to_string_lossy().replace('\\', "/")) -} - -fn parse_git_name_status( - output: &str, -) -> Result, MemoryWorkspaceGitError> { +fn collect_workspace_changes(diff: &Diff<'_>) -> Vec { let mut changes = Vec::new(); - for line in output.lines().filter(|line| !line.trim().is_empty()) { - let mut parts = line.splitn(2, char::is_whitespace); - let status = parts.next().unwrap_or_default(); - let path = parts.next().unwrap_or_default().trim(); - let status = match status.chars().next() { - Some('A') => MemoryWorkspaceChangeStatus::Added, - Some('M') => MemoryWorkspaceChangeStatus::Modified, - Some('D') => MemoryWorkspaceChangeStatus::Deleted, - Some(status) => { - return Err(MemoryWorkspaceGitError::UnsupportedGitStatus { - status, - line: line.to_string(), - }); + for delta in diff.deltas() { + let (status, path) = match delta.status() { + Delta::Added | Delta::Untracked | Delta::Ignored => { + (MemoryWorkspaceChangeStatus::Added, delta.new_file().path()) } - None => continue, + Delta::Deleted => ( + MemoryWorkspaceChangeStatus::Deleted, + delta.old_file().path(), + ), + Delta::Modified + | Delta::Renamed + | Delta::Copied + | Delta::Typechange + | Delta::Unreadable + | Delta::Conflicted => ( + MemoryWorkspaceChangeStatus::Modified, + delta.new_file().path().or_else(|| delta.old_file().path()), + ), + Delta::Unmodified => continue, }; - if !path.is_empty() { + if let Some(path) = path { changes.push(MemoryWorkspaceChange { status, - path: path.replace('\\', "/"), + path: path.to_string_lossy().replace('\\', "/"), }); } } - Ok(changes) + changes.sort_by(|left, right| left.path.cmp(&right.path)); + changes.dedup_by(|left, right| left.path == right.path); + changes } -fn parse_nul_paths(output: &[u8]) -> BTreeSet { - output - .split(|byte| *byte == 0) - .filter(|part| !part.is_empty()) - .map(|part| String::from_utf8_lossy(part).replace('\\', "/")) - .collect() +fn ignored_workspace_paths(diff: &Diff<'_>) -> Vec { + let mut paths = diff + .deltas() + .filter(|delta| delta.status() == Delta::Ignored) + .filter_map(|delta| delta.new_file().path()) + .map(|path| path.to_string_lossy().replace('\\', "/")) + .collect::>(); + paths.sort(); + paths.dedup(); + paths } fn render_added_file_diff(root: &Path, path: &str) -> Result { @@ -370,6 +314,19 @@ fn render_added_file_diff(root: &Path, path: &str) -> Result, root: &Path) -> Result { + let mut rendered = Vec::new(); + diff.print(DiffFormat::Patch, |_, _, line| { + if matches!(line.origin(), ' ' | '+' | '-') { + rendered.push(line.origin() as u8); + } + rendered.extend_from_slice(line.content()); + true + }) + .map_err(|source| git_operation(root, "render workspace diff", source))?; + String::from_utf8(rendered).map_err(|source| MemoryWorkspaceGitError::DiffOutputUtf8 { source }) +} + fn append_bounded_diff(rendered: &mut String, diff: &str) { if diff.len() <= MEMORY_WORKSPACE_DIFF_MAX_BYTES { rendered.push_str(diff); @@ -401,47 +358,15 @@ fn previous_char_boundary(value: &str, max_bytes: usize) -> usize { index } -fn git_stdout(root: &Path, args: &[&str]) -> Result { - let output = run_git(root, args)?; - String::from_utf8(output.stdout) - .map_err(|source| MemoryWorkspaceGitError::GitOutputUtf8 { source }) -} - -fn git_z_stdout(root: &Path, args: &[&str]) -> Result, MemoryWorkspaceGitError> { - run_git(root, args).map(|output| output.stdout) -} - -fn run_git(root: &Path, args: &[&str]) -> Result { - let mut full_args = vec![ - "-c", - "user.name=BitFun", - "-c", - "user.email=bitfun@localhost", - ]; - full_args.extend_from_slice(args); - run_git_raw(root, &full_args) -} - -fn run_git_raw( +fn git_operation( root: &Path, - args: &[&str], -) -> Result { - let output = Command::new("git") - .arg("-C") - .arg(root) - .args(args) - .output() - .map_err(|source| MemoryWorkspaceGitError::RunGit { - root: root.to_path_buf(), - source, - })?; - if output.status.success() { - Ok(output) - } else { - Err(MemoryWorkspaceGitError::GitFailed { - root: root.to_path_buf(), - stderr: String::from_utf8_lossy(&output.stderr).to_string(), - }) + operation: &'static str, + source: git2::Error, +) -> MemoryWorkspaceGitError { + MemoryWorkspaceGitError::GitOperation { + root: root.to_path_buf(), + operation, + source, } } @@ -485,10 +410,40 @@ mod tests { status: MemoryWorkspaceChangeStatus::Added, path: "rollout_summaries/new.md".to_string(), })); + assert!(diff.unified_diff.contains("diff --git")); + assert!(diff.unified_diff.contains("+new index")); + assert!(diff.unified_diff.contains("-old summary")); assert!(diff.unified_diff.contains("new index")); assert!(diff.unified_diff.contains("new summary")); } + #[tokio::test] + async fn memory_workspace_diff_includes_ignored_ad_hoc_notes() { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path(); + std::fs::write( + root.join(".gitignore"), + "extensions/ad_hoc/notes/ignored-note.md\n", + ) + .unwrap(); + reset_memory_workspace_git_baseline(root).await.unwrap(); + + let note_path = root.join("extensions/ad_hoc/notes/ignored-note.md"); + std::fs::create_dir_all(note_path.parent().unwrap()).unwrap(); + std::fs::write(¬e_path, "Durable ignored memory note\n").unwrap(); + + let diff = memory_workspace_diff(root).await.unwrap(); + + assert!(diff.changes.contains(&MemoryWorkspaceChange { + status: MemoryWorkspaceChangeStatus::Added, + path: "extensions/ad_hoc/notes/ignored-note.md".to_string(), + })); + assert!(diff + .unified_diff + .contains("extensions/ad_hoc/notes/ignored-note.md")); + assert!(diff.unified_diff.contains("+Durable ignored memory note")); + } + #[test] fn memory_workspace_diff_renderer_preserves_legacy_status_header() { let rendered = render_memory_workspace_diff_file(&super::MemoryWorkspaceDiff { From f3db6312feef8536e88b4fd666f4843b1556bebf Mon Sep 17 00:00:00 2001 From: wsp Date: Mon, 27 Jul 2026 18:34:07 +0800 Subject: [PATCH 2/2] fix(windows): hide console windows for background commands - Hide MiniApp worker command windows on Windows. - Apply CREATE_NO_WINDOW to native hook execution. - Route local workspace shell and Docker workspace processes through the shared hidden-window command factory. - Cover local Docker probes, signals, and long-lived stdio transport execution. --- .../core-boundaries/rules/feature-rules.mjs | 2 +- src/apps/desktop/resources/worker_host.js | 6 +++++- .../agent-runtime/src/native_hooks/engine.rs | 7 +++++++ .../services/services-core/src/workspace.rs | 2 +- .../services/services-integrations/Cargo.toml | 1 + .../src/remote_ssh/manager.rs | 18 +++++++++++------- .../src/remote_ssh/transport.rs | 4 ++-- 7 files changed, 28 insertions(+), 12 deletions(-) diff --git a/scripts/core-boundaries/rules/feature-rules.mjs b/scripts/core-boundaries/rules/feature-rules.mjs index bd3219bbdf..1ef4a602d9 100644 --- a/scripts/core-boundaries/rules/feature-rules.mjs +++ b/scripts/core-boundaries/rules/feature-rules.mjs @@ -75,7 +75,7 @@ export const optionalDependencyFeatureOwnerRules = [ { depName: 'bitfun-runtime-ports', ownerFeatures: ['remote-connect', 'remote-ssh', 'remote-ssh-concrete', 'script-tool-runtime'] }, { depName: 'bitfun-services-core', - ownerFeatures: ['browser-control', 'git', 'mcp', 'miniapp-runtime', 'process-tree', 'remote-connect', 'review-platform', 'workspace-search'], + ownerFeatures: ['browser-control', 'git', 'mcp', 'miniapp-runtime', 'process-tree', 'remote-connect', 'remote-ssh-concrete', 'review-platform', 'workspace-search'], }, { depName: 'bzip2', ownerFeatures: ['speech'] }, { depName: 'chrono', ownerFeatures: ['debug-log', 'git', 'remote-connect', 'remote-ssh-concrete', 'review-platform', 'speech'] }, diff --git a/src/apps/desktop/resources/worker_host.js b/src/apps/desktop/resources/worker_host.js index c8f6614201..42c5c7050a 100644 --- a/src/apps/desktop/resources/worker_host.js +++ b/src/apps/desktop/resources/worker_host.js @@ -133,7 +133,11 @@ async function dispatch(method, params) { if (allow.length > 0 && !allow.some((a) => a.toLowerCase() === base.toLowerCase())) { throw new Error('Command not in allowlist'); } - const opts = { cwd: params.cwd || appDir, timeout: params.timeout || 30000 }; + const opts = { + cwd: params.cwd || appDir, + timeout: params.timeout || 30000, + windowsHide: process.platform === 'win32' + }; const { stdout, stderr } = await execAsync(params.command || '', opts); return { stdout, stderr, exit_code: 0 }; } diff --git a/src/crates/execution/agent-runtime/src/native_hooks/engine.rs b/src/crates/execution/agent-runtime/src/native_hooks/engine.rs index 58748860d4..1d406000a3 100644 --- a/src/crates/execution/agent-runtime/src/native_hooks/engine.rs +++ b/src/crates/execution/agent-runtime/src/native_hooks/engine.rs @@ -181,6 +181,13 @@ async fn run_hook_command( if let Some(cwd) = existing_dir(cwd) { process.current_dir(cwd); } + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + process.as_std_mut().creation_flags(CREATE_NO_WINDOW); + } process .stdin(Stdio::piped()) .stdout(Stdio::piped()) diff --git a/src/crates/services/services-core/src/workspace.rs b/src/crates/services/services-core/src/workspace.rs index 5bea07a6e1..7e847ef2a0 100644 --- a/src/crates/services/services-core/src/workspace.rs +++ b/src/crates/services/services-core/src/workspace.rs @@ -92,7 +92,7 @@ impl WorkspaceShell for LocalWorkspaceShell { use std::process::Stdio; use tokio::io::AsyncReadExt; - let mut cmd = tokio::process::Command::new("sh"); + let mut cmd = crate::process_manager::create_tokio_command("sh"); cmd.arg("-c").arg(command); cmd.current_dir(&self.workspace_root); cmd.stdout(Stdio::piped()); diff --git a/src/crates/services/services-integrations/Cargo.toml b/src/crates/services/services-integrations/Cargo.toml index 6603d4a72c..5be2181575 100644 --- a/src/crates/services/services-integrations/Cargo.toml +++ b/src/crates/services/services-integrations/Cargo.toml @@ -190,6 +190,7 @@ remote-ssh-concrete = [ "anyhow", "async-trait", "base64", + "bitfun-services-core", "bitfun-runtime-ports", "chrono", "dirs", diff --git a/src/crates/services/services-integrations/src/remote_ssh/manager.rs b/src/crates/services/services-integrations/src/remote_ssh/manager.rs index 05dd8f010e..dc3d8d0390 100644 --- a/src/crates/services/services-integrations/src/remote_ssh/manager.rs +++ b/src/crates/services/services-integrations/src/remote_ssh/manager.rs @@ -10,6 +10,7 @@ use crate::remote_ssh::types::{ }; use anyhow::{anyhow, Context}; use async_trait::async_trait; +use bitfun_services_core::process_manager; use russh::client::{DisconnectReason, Handle, Handler, Msg}; use russh::Sig; use russh_keys::key::{KeyPair, PublicKey}; @@ -25,7 +26,6 @@ use std::sync::Arc; use std::sync::Once; use tokio::io::{AsyncRead, AsyncWrite}; use tokio::net::TcpStream; -use tokio::process::Command; use tokio::time::{Duration, Instant}; const SSH_COMMAND_WAIT_POLL_INTERVAL: Duration = Duration::from_millis(100); @@ -1190,7 +1190,7 @@ fn local_container_signal_hook( let command = container_signal_command(&pid_file, signal); let output = tokio::time::timeout( Duration::from_secs(3), - Command::new(&container.docker_path) + process_manager::create_tokio_command(&container.docker_path) .args(docker_exec_args(&container, &command, false)) .stdin(Stdio::null()) .stdout(Stdio::null()) @@ -1945,7 +1945,7 @@ impl SSHConnectionManager { let output = if container.local { tokio::time::timeout( Duration::from_secs(config.options.connect_timeout_secs.max(1)), - Command::new(&container.docker_path) + process_manager::create_tokio_command(&container.docker_path) .args(["ps", "-a", "--format", format]) .output(), ) @@ -2102,7 +2102,9 @@ impl SSHConnectionManager { None => { (stage.id.starts_with("jump-") && error_text.contains(&stage.label)) || (stage.id == "container" - && (error_text.to_ascii_lowercase().contains("docker container") + && (error_text + .to_ascii_lowercase() + .contains("docker container") || error_text .to_ascii_lowercase() .contains("container sshd"))) @@ -2727,7 +2729,7 @@ impl SSHConnectionManager { let container = config.container.as_ref()?; let output = tokio::time::timeout( Duration::from_secs(timeout_secs), - Command::new(&container.docker_path) + process_manager::create_tokio_command(&container.docker_path) .args(["port", &container.name, "22/tcp"]) .output(), ) @@ -3065,7 +3067,9 @@ impl SSHConnectionManager { ); let output = tokio::time::timeout( Duration::from_secs(timeout_secs), - Command::new(&container.docker_path).args(args).output(), + process_manager::create_tokio_command(&container.docker_path) + .args(args) + .output(), ) .await .map_err(|_| { @@ -3507,7 +3511,7 @@ impl SSHConnectionManager { matches!( tokio::time::timeout( Duration::from_secs(3), - Command::new(&container.docker_path) + process_manager::create_tokio_command(&container.docker_path) .args(["inspect", "--format", "{{.State.Running}}", &container.name]) .output(), ) diff --git a/src/crates/services/services-integrations/src/remote_ssh/transport.rs b/src/crates/services/services-integrations/src/remote_ssh/transport.rs index ed0388190c..75308bab05 100644 --- a/src/crates/services/services-integrations/src/remote_ssh/transport.rs +++ b/src/crates/services/services-integrations/src/remote_ssh/transport.rs @@ -5,6 +5,7 @@ //! stdin, stdout, stderr, exit status, and interrupt/kill control. use anyhow::{anyhow, Context}; +use bitfun_services_core::process_manager; #[cfg(feature = "remote-ssh-concrete")] use russh::client::Msg; #[cfg(feature = "remote-ssh-concrete")] @@ -16,7 +17,6 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::task::{Context as TaskContext, Poll}; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, DuplexStream, ReadBuf}; -use tokio::process::Command; use tokio::sync::{mpsc, watch}; use tokio_util::sync::CancellationToken; @@ -146,7 +146,7 @@ impl WorkspaceStdio { args: &[String], signal_hook: Option, ) -> anyhow::Result { - let mut child = Command::new(executable) + let mut child = process_manager::create_tokio_command(executable) .args(args) .stdin(Stdio::piped()) .stdout(Stdio::piped())