mirror of
https://github.com/openai/codex.git
synced 2026-02-01 22:47:52 +00:00
Compare commits
11 Commits
1271d450b1
...
rust-v0.88
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa47844d2b | ||
|
|
3c28c85063 | ||
|
|
dc1b62acbd | ||
|
|
186794dbb3 | ||
|
|
7ebe13f692 | ||
|
|
a803467f52 | ||
|
|
a5e5d7a384 | ||
|
|
66b74efbc6 | ||
|
|
78a359f7fa | ||
|
|
274af30525 | ||
|
|
efa9326f08 |
47
codex-rs/Cargo.lock
generated
47
codex-rs/Cargo.lock
generated
@@ -360,16 +360,19 @@ dependencies = [
|
||||
"objc2-foundation",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.60.2",
|
||||
"wl-clipboard-rs",
|
||||
"x11rb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
version = "1.7.1"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
|
||||
checksum = "51d03449bb8ca2cc2ef70869af31463d1ae5ccc8fa3e334b307203fbf815207e"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
@@ -861,9 +864,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.42"
|
||||
version = "0.4.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
|
||||
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
@@ -1298,7 +1301,7 @@ dependencies = [
|
||||
"codex-windows-sandbox",
|
||||
"core-foundation 0.9.4",
|
||||
"core_test_support",
|
||||
"ctor 0.5.0",
|
||||
"ctor 0.6.3",
|
||||
"dirs",
|
||||
"dunce",
|
||||
"encoding_rs",
|
||||
@@ -1681,7 +1684,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"codex-process-hardening",
|
||||
"ctor 0.5.0",
|
||||
"ctor 0.6.3",
|
||||
"libc",
|
||||
"reqwest",
|
||||
"serde",
|
||||
@@ -2262,9 +2265,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ctor"
|
||||
version = "0.5.0"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67773048316103656a637612c4a62477603b777d91d9c62ff2290f9cde178fdb"
|
||||
checksum = "424e0138278faeb2b401f174ad17e715c829512d74f3d1e81eb43365c2e0590e"
|
||||
dependencies = [
|
||||
"ctor-proc-macro",
|
||||
"dtor",
|
||||
@@ -2272,9 +2275,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ctor-proc-macro"
|
||||
version = "0.0.6"
|
||||
version = "0.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2"
|
||||
checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1"
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
@@ -2829,7 +2832,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2926,7 +2929,7 @@ checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"rustix 1.0.8",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3867,7 +3870,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4160,9 +4163,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.28"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "logos"
|
||||
@@ -5378,7 +5381,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"socket2 0.6.1",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5757,7 +5760,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.15",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5770,7 +5773,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.9.4",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7071,9 +7074,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.48.0"
|
||||
version = "1.49.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
|
||||
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
@@ -8088,7 +8091,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -51,7 +51,7 @@ members = [
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.0.0"
|
||||
version = "0.88.0-alpha.5"
|
||||
# Track the edition for all workspace crates in one place. Individual
|
||||
# crates can still override this value, but keeping it here means new
|
||||
# crates created with `cargo new -w ...` automatically inherit the 2024
|
||||
@@ -122,12 +122,12 @@ axum = { version = "0.8", default-features = false }
|
||||
base64 = "0.22.1"
|
||||
bytes = "1.10.1"
|
||||
chardetng = "0.1.17"
|
||||
chrono = "0.4.42"
|
||||
chrono = "0.4.43"
|
||||
clap = "4"
|
||||
clap_complete = "4"
|
||||
color-eyre = "0.6.3"
|
||||
crossterm = "0.28.1"
|
||||
ctor = "0.5.0"
|
||||
ctor = "0.6.3"
|
||||
derive_more = "2"
|
||||
diffy = "0.4.2"
|
||||
dirs = "6"
|
||||
|
||||
@@ -18,7 +18,7 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
arc-swap = "1.7.1"
|
||||
arc-swap = "1.8.0"
|
||||
async-channel = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
|
||||
@@ -85,7 +85,6 @@ impl AgentControl {
|
||||
result
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // Will be used for collab tools.
|
||||
/// Fetch the last known status for `agent_id`, returning `NotFound` when unavailable.
|
||||
pub(crate) async fn get_status(&self, agent_id: ThreadId) -> AgentStatus {
|
||||
let Ok(state) = self.upgrade() else {
|
||||
|
||||
@@ -525,7 +525,6 @@ impl Session {
|
||||
session_configuration.collaboration_mode.model(),
|
||||
model_info.slug.as_str(),
|
||||
);
|
||||
|
||||
let per_turn_config = Arc::new(per_turn_config);
|
||||
let client = ModelClient::new(
|
||||
per_turn_config.clone(),
|
||||
|
||||
@@ -135,6 +135,13 @@ async fn run_shell_script_with_timeout(
|
||||
// returns a ref of handler.
|
||||
let mut handler = Command::new(&args[0]);
|
||||
handler.args(&args[1..]);
|
||||
#[cfg(unix)]
|
||||
unsafe {
|
||||
handler.pre_exec(|| {
|
||||
codex_utils_pty::process_group::detach_from_tty()?;
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
handler.kill_on_drop(true);
|
||||
let output = timeout(snapshot_timeout, handler.output())
|
||||
.await
|
||||
|
||||
@@ -66,12 +66,12 @@ pub(crate) async fn spawn_child_async(
|
||||
|
||||
#[cfg(unix)]
|
||||
unsafe {
|
||||
let set_process_group = matches!(stdio_policy, StdioPolicy::RedirectForShellTool);
|
||||
let detach_from_tty = matches!(stdio_policy, StdioPolicy::RedirectForShellTool);
|
||||
#[cfg(target_os = "linux")]
|
||||
let parent_pid = libc::getpid();
|
||||
cmd.pre_exec(move || {
|
||||
if set_process_group {
|
||||
codex_utils_pty::process_group::set_process_group()?;
|
||||
if detach_from_tty {
|
||||
codex_utils_pty::process_group::detach_from_tty()?;
|
||||
}
|
||||
|
||||
// This relies on prctl(2), so it only works on Linux.
|
||||
|
||||
@@ -37,7 +37,6 @@ pub(crate) enum TaskKind {
|
||||
Compact,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct RunningTask {
|
||||
pub(crate) done: Arc<Notify>,
|
||||
pub(crate) kind: TaskKind,
|
||||
@@ -45,6 +44,8 @@ pub(crate) struct RunningTask {
|
||||
pub(crate) cancellation_token: CancellationToken,
|
||||
pub(crate) handle: Arc<AbortOnDropHandle<()>>,
|
||||
pub(crate) turn_context: Arc<TurnContext>,
|
||||
// Timer recorded when the task drops to capture the full turn duration.
|
||||
pub(crate) _timer: Option<codex_otel::Timer>,
|
||||
}
|
||||
|
||||
impl ActiveTurn {
|
||||
|
||||
@@ -144,6 +144,12 @@ impl Session {
|
||||
})
|
||||
};
|
||||
|
||||
let timer = turn_context
|
||||
.client
|
||||
.get_otel_manager()
|
||||
.start_timer("codex.turn.e2e_duration_ms", &[])
|
||||
.ok();
|
||||
|
||||
let running_task = RunningTask {
|
||||
done,
|
||||
handle: Arc::new(AbortOnDropHandle::new(handle)),
|
||||
@@ -151,6 +157,7 @@ impl Session {
|
||||
task,
|
||||
cancellation_token,
|
||||
turn_context: Arc::clone(&turn_context),
|
||||
_timer: timer,
|
||||
};
|
||||
self.register_new_active_task(running_task).await;
|
||||
}
|
||||
|
||||
@@ -235,6 +235,15 @@ impl ThreadManager {
|
||||
self.state.threads.write().await.remove(thread_id)
|
||||
}
|
||||
|
||||
/// Closes all threads open in this ThreadManager
|
||||
pub async fn remove_and_close_all_threads(&self) -> CodexResult<()> {
|
||||
for thread in self.state.threads.read().await.values() {
|
||||
thread.submit(Op::Shutdown).await?;
|
||||
}
|
||||
self.state.threads.write().await.clear();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fork an existing thread by taking messages up to the given position (not including
|
||||
/// the message at the given position) and starting a new thread with identical
|
||||
/// configuration (unless overridden by the caller's `config`). The new thread will have
|
||||
|
||||
@@ -14,10 +14,12 @@ You are Codex Orchestrator, based on GPT-5. You are running as an orchestration
|
||||
* **Never stop monitoring workers.**
|
||||
* **Do not rush workers. Be patient.**
|
||||
* The orchestrator must not return unless the task is fully accomplished.
|
||||
* If the user ask you a question/status while you are working, always answer him before continuing your work.
|
||||
|
||||
## Worker execution semantics
|
||||
|
||||
* While a worker is running, you cannot observe intermediate state.
|
||||
* Workers are able to run commands, update/create/delete files etc. They can be considered as fully autonomous agents
|
||||
* Messages sent with `send_input` are queued and processed only after the worker finishes, unless interrupted.
|
||||
* Therefore:
|
||||
* Do not send messages to “check status” or “ask for progress” unless being asked.
|
||||
@@ -40,7 +42,7 @@ You are Codex Orchestrator, based on GPT-5. You are running as an orchestration
|
||||
* verify correctness,
|
||||
* check integration with other work,
|
||||
* assess whether the global task is closer to completion.
|
||||
5. If issues remain, assign fixes to the appropriate worker(s) and repeat steps 3–5.
|
||||
5. If issues remain, assign fixes to the appropriate worker(s) and repeat steps 3–5. Do not fix yourself unless the fixes are very small.
|
||||
6. Close agents only when no further work is required from them.
|
||||
7. Return to the user only when the task is fully completed and verified.
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::metrics::MetricsClient;
|
||||
use crate::metrics::MetricsConfig;
|
||||
use crate::metrics::MetricsError;
|
||||
use crate::metrics::Result as MetricsResult;
|
||||
use crate::metrics::timer::Timer;
|
||||
pub use crate::metrics::timer::Timer;
|
||||
use crate::metrics::validation::validate_tag_key;
|
||||
use crate::metrics::validation::validate_tag_value;
|
||||
use crate::otel_provider::OtelProvider;
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::metrics::MetricsClient;
|
||||
use crate::metrics::error::Result;
|
||||
use std::time::Instant;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Timer {
|
||||
name: String,
|
||||
tags: Vec<(String, String)>,
|
||||
|
||||
@@ -672,6 +672,9 @@ impl App {
|
||||
let summary =
|
||||
session_summary(self.chat_widget.token_usage(), self.chat_widget.thread_id());
|
||||
self.shutdown_current_thread().await;
|
||||
if let Err(err) = self.server.remove_and_close_all_threads().await {
|
||||
tracing::warn!(error = %err, "failed to close all threads");
|
||||
}
|
||||
let init = crate::chatwidget::ChatWidgetInit {
|
||||
config: self.config.clone(),
|
||||
frame_requester: tui.frame_requester(),
|
||||
|
||||
@@ -244,9 +244,14 @@ pub fn paste_image_to_temp_png() -> Result<(PathBuf, PastedImageInfo), PasteImag
|
||||
/// - shell-escaped single paths (via `shlex`)
|
||||
pub fn normalize_pasted_path(pasted: &str) -> Option<PathBuf> {
|
||||
let pasted = pasted.trim();
|
||||
let unquoted = pasted
|
||||
.strip_prefix('"')
|
||||
.and_then(|s| s.strip_suffix('"'))
|
||||
.or_else(|| pasted.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
|
||||
.unwrap_or(pasted);
|
||||
|
||||
// file:// URL → filesystem path
|
||||
if let Ok(url) = url::Url::parse(pasted)
|
||||
if let Ok(url) = url::Url::parse(unquoted)
|
||||
&& url.scheme() == "file"
|
||||
{
|
||||
return url.to_file_path().ok();
|
||||
@@ -258,38 +263,18 @@ pub fn normalize_pasted_path(pasted: &str) -> Option<PathBuf> {
|
||||
// Detect unquoted Windows paths and bypass POSIX shlex which
|
||||
// treats backslashes as escapes (e.g., C:\Users\Alice\file.png).
|
||||
// Also handles UNC paths (\\server\share\path).
|
||||
let looks_like_windows_path = {
|
||||
// Drive letter path: C:\ or C:/
|
||||
let drive = pasted
|
||||
.chars()
|
||||
.next()
|
||||
.map(|c| c.is_ascii_alphabetic())
|
||||
.unwrap_or(false)
|
||||
&& pasted.get(1..2) == Some(":")
|
||||
&& pasted
|
||||
.get(2..3)
|
||||
.map(|s| s == "\\" || s == "/")
|
||||
.unwrap_or(false);
|
||||
// UNC path: \\server\share
|
||||
let unc = pasted.starts_with("\\\\");
|
||||
drive || unc
|
||||
};
|
||||
if looks_like_windows_path {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if is_probably_wsl()
|
||||
&& let Some(converted) = convert_windows_path_to_wsl(pasted)
|
||||
{
|
||||
return Some(converted);
|
||||
}
|
||||
}
|
||||
return Some(PathBuf::from(pasted));
|
||||
if let Some(path) = normalize_windows_path(unquoted) {
|
||||
return Some(path);
|
||||
}
|
||||
|
||||
// shell-escaped single path → unescaped
|
||||
let parts: Vec<String> = shlex::Shlex::new(pasted).collect();
|
||||
if parts.len() == 1 {
|
||||
return parts.into_iter().next().map(PathBuf::from);
|
||||
let part = parts.into_iter().next()?;
|
||||
if let Some(path) = normalize_windows_path(&part) {
|
||||
return Some(path);
|
||||
}
|
||||
return Some(PathBuf::from(part));
|
||||
}
|
||||
|
||||
None
|
||||
@@ -339,6 +324,36 @@ fn convert_windows_path_to_wsl(input: &str) -> Option<PathBuf> {
|
||||
Some(result)
|
||||
}
|
||||
|
||||
fn normalize_windows_path(input: &str) -> Option<PathBuf> {
|
||||
// Drive letter path: C:\ or C:/
|
||||
let drive = input
|
||||
.chars()
|
||||
.next()
|
||||
.map(|c| c.is_ascii_alphabetic())
|
||||
.unwrap_or(false)
|
||||
&& input.get(1..2) == Some(":")
|
||||
&& input
|
||||
.get(2..3)
|
||||
.map(|s| s == "\\" || s == "/")
|
||||
.unwrap_or(false);
|
||||
// UNC path: \\server\share
|
||||
let unc = input.starts_with("\\\\");
|
||||
if !drive && !unc {
|
||||
return None;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if is_probably_wsl()
|
||||
&& let Some(converted) = convert_windows_path_to_wsl(input)
|
||||
{
|
||||
return Some(converted);
|
||||
}
|
||||
}
|
||||
|
||||
Some(PathBuf::from(input))
|
||||
}
|
||||
|
||||
/// Infer an image format for the provided path based on its extension.
|
||||
pub fn pasted_image_format(path: &Path) -> EncodedImageFormat {
|
||||
match path
|
||||
@@ -438,9 +453,39 @@ mod pasted_paths_tests {
|
||||
#[test]
|
||||
fn normalize_single_quoted_windows_path() {
|
||||
let input = r"'C:\\Users\\Alice\\My File.jpeg'";
|
||||
let unquoted = r"C:\\Users\\Alice\\My File.jpeg";
|
||||
let result =
|
||||
normalize_pasted_path(input).expect("should trim single quotes on windows path");
|
||||
assert_eq!(result, PathBuf::from(r"C:\\Users\\Alice\\My File.jpeg"));
|
||||
#[cfg(target_os = "linux")]
|
||||
let expected = if is_probably_wsl()
|
||||
&& let Some(converted) = convert_windows_path_to_wsl(unquoted)
|
||||
{
|
||||
converted
|
||||
} else {
|
||||
PathBuf::from(unquoted)
|
||||
};
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let expected = PathBuf::from(unquoted);
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_double_quoted_windows_path() {
|
||||
let input = r#""C:\\Users\\Alice\\My File.jpeg""#;
|
||||
let unquoted = r"C:\\Users\\Alice\\My File.jpeg";
|
||||
let result =
|
||||
normalize_pasted_path(input).expect("should trim double quotes on windows path");
|
||||
#[cfg(target_os = "linux")]
|
||||
let expected = if is_probably_wsl()
|
||||
&& let Some(converted) = convert_windows_path_to_wsl(unquoted)
|
||||
{
|
||||
converted
|
||||
} else {
|
||||
PathBuf::from(unquoted)
|
||||
};
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let expected = PathBuf::from(unquoted);
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1447,6 +1447,9 @@ impl App {
|
||||
self.chat_widget.conversation_id(),
|
||||
);
|
||||
self.shutdown_current_conversation().await;
|
||||
if let Err(err) = self.server.remove_and_close_all_threads().await {
|
||||
tracing::warn!(error = %err, "failed to close all threads");
|
||||
}
|
||||
let init = crate::chatwidget::ChatWidgetInit {
|
||||
config: self.config.clone(),
|
||||
frame_requester: tui.frame_requester(),
|
||||
|
||||
@@ -244,9 +244,14 @@ pub fn paste_image_to_temp_png() -> Result<(PathBuf, PastedImageInfo), PasteImag
|
||||
/// - shell-escaped single paths (via `shlex`)
|
||||
pub fn normalize_pasted_path(pasted: &str) -> Option<PathBuf> {
|
||||
let pasted = pasted.trim();
|
||||
let unquoted = pasted
|
||||
.strip_prefix('"')
|
||||
.and_then(|s| s.strip_suffix('"'))
|
||||
.or_else(|| pasted.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
|
||||
.unwrap_or(pasted);
|
||||
|
||||
// file:// URL → filesystem path
|
||||
if let Ok(url) = url::Url::parse(pasted)
|
||||
if let Ok(url) = url::Url::parse(unquoted)
|
||||
&& url.scheme() == "file"
|
||||
{
|
||||
return url.to_file_path().ok();
|
||||
@@ -258,38 +263,18 @@ pub fn normalize_pasted_path(pasted: &str) -> Option<PathBuf> {
|
||||
// Detect unquoted Windows paths and bypass POSIX shlex which
|
||||
// treats backslashes as escapes (e.g., C:\Users\Alice\file.png).
|
||||
// Also handles UNC paths (\\server\share\path).
|
||||
let looks_like_windows_path = {
|
||||
// Drive letter path: C:\ or C:/
|
||||
let drive = pasted
|
||||
.chars()
|
||||
.next()
|
||||
.map(|c| c.is_ascii_alphabetic())
|
||||
.unwrap_or(false)
|
||||
&& pasted.get(1..2) == Some(":")
|
||||
&& pasted
|
||||
.get(2..3)
|
||||
.map(|s| s == "\\" || s == "/")
|
||||
.unwrap_or(false);
|
||||
// UNC path: \\server\share
|
||||
let unc = pasted.starts_with("\\\\");
|
||||
drive || unc
|
||||
};
|
||||
if looks_like_windows_path {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if is_probably_wsl()
|
||||
&& let Some(converted) = convert_windows_path_to_wsl(pasted)
|
||||
{
|
||||
return Some(converted);
|
||||
}
|
||||
}
|
||||
return Some(PathBuf::from(pasted));
|
||||
if let Some(path) = normalize_windows_path(unquoted) {
|
||||
return Some(path);
|
||||
}
|
||||
|
||||
// shell-escaped single path → unescaped
|
||||
let parts: Vec<String> = shlex::Shlex::new(pasted).collect();
|
||||
if parts.len() == 1 {
|
||||
return parts.into_iter().next().map(PathBuf::from);
|
||||
let part = parts.into_iter().next()?;
|
||||
if let Some(path) = normalize_windows_path(&part) {
|
||||
return Some(path);
|
||||
}
|
||||
return Some(PathBuf::from(part));
|
||||
}
|
||||
|
||||
None
|
||||
@@ -339,6 +324,36 @@ fn convert_windows_path_to_wsl(input: &str) -> Option<PathBuf> {
|
||||
Some(result)
|
||||
}
|
||||
|
||||
fn normalize_windows_path(input: &str) -> Option<PathBuf> {
|
||||
// Drive letter path: C:\ or C:/
|
||||
let drive = input
|
||||
.chars()
|
||||
.next()
|
||||
.map(|c| c.is_ascii_alphabetic())
|
||||
.unwrap_or(false)
|
||||
&& input.get(1..2) == Some(":")
|
||||
&& input
|
||||
.get(2..3)
|
||||
.map(|s| s == "\\" || s == "/")
|
||||
.unwrap_or(false);
|
||||
// UNC path: \\server\share
|
||||
let unc = input.starts_with("\\\\");
|
||||
if !drive && !unc {
|
||||
return None;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if is_probably_wsl()
|
||||
&& let Some(converted) = convert_windows_path_to_wsl(input)
|
||||
{
|
||||
return Some(converted);
|
||||
}
|
||||
}
|
||||
|
||||
Some(PathBuf::from(input))
|
||||
}
|
||||
|
||||
/// Infer an image format for the provided path based on its extension.
|
||||
pub fn pasted_image_format(path: &Path) -> EncodedImageFormat {
|
||||
match path
|
||||
@@ -438,9 +453,39 @@ mod pasted_paths_tests {
|
||||
#[test]
|
||||
fn normalize_single_quoted_windows_path() {
|
||||
let input = r"'C:\\Users\\Alice\\My File.jpeg'";
|
||||
let unquoted = r"C:\\Users\\Alice\\My File.jpeg";
|
||||
let result =
|
||||
normalize_pasted_path(input).expect("should trim single quotes on windows path");
|
||||
assert_eq!(result, PathBuf::from(r"C:\\Users\\Alice\\My File.jpeg"));
|
||||
#[cfg(target_os = "linux")]
|
||||
let expected = if is_probably_wsl()
|
||||
&& let Some(converted) = convert_windows_path_to_wsl(unquoted)
|
||||
{
|
||||
converted
|
||||
} else {
|
||||
PathBuf::from(unquoted)
|
||||
};
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let expected = PathBuf::from(unquoted);
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_double_quoted_windows_path() {
|
||||
let input = r#""C:\\Users\\Alice\\My File.jpeg""#;
|
||||
let unquoted = r"C:\\Users\\Alice\\My File.jpeg";
|
||||
let result =
|
||||
normalize_pasted_path(input).expect("should trim double quotes on windows path");
|
||||
#[cfg(target_os = "linux")]
|
||||
let expected = if is_probably_wsl()
|
||||
&& let Some(converted) = convert_windows_path_to_wsl(unquoted)
|
||||
{
|
||||
converted
|
||||
} else {
|
||||
PathBuf::from(unquoted)
|
||||
};
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let expected = PathBuf::from(unquoted);
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -118,7 +118,7 @@ async fn spawn_process_with_stdin_mode(
|
||||
#[cfg(unix)]
|
||||
unsafe {
|
||||
command.pre_exec(move || {
|
||||
crate::process_group::set_process_group()?;
|
||||
crate::process_group::detach_from_tty()?;
|
||||
#[cfg(target_os = "linux")]
|
||||
crate::process_group::set_parent_death_signal(parent_pid)?;
|
||||
Ok(())
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
//! command can be cleaned up reliably:
|
||||
//! - `set_process_group` is called in `pre_exec` so the child starts its own
|
||||
//! process group.
|
||||
//! - `detach_from_tty` starts a new session so non-interactive children do not
|
||||
//! inherit the controlling TTY.
|
||||
//! - `kill_process_group_by_pid` targets the whole group (children/grandchildren)
|
||||
//! - `kill_process_group` targets a known process group ID directly
|
||||
//! instead of a single PID.
|
||||
@@ -42,6 +44,26 @@ pub fn set_parent_death_signal(_parent_pid: i32) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
/// Detach from the controlling TTY by starting a new session.
|
||||
pub fn detach_from_tty() -> io::Result<()> {
|
||||
let result = unsafe { libc::setsid() };
|
||||
if result == -1 {
|
||||
let err = io::Error::last_os_error();
|
||||
if err.raw_os_error() == Some(libc::EPERM) {
|
||||
return set_process_group();
|
||||
}
|
||||
return Err(err);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
/// No-op on non-Unix platforms.
|
||||
pub fn detach_from_tty() -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
/// Put the calling process into its own process group.
|
||||
///
|
||||
|
||||
@@ -152,6 +152,49 @@ async fn pipe_process_round_trips_stdin() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn pipe_process_detaches_from_parent_session() -> anyhow::Result<()> {
|
||||
let parent_sid = unsafe { libc::getsid(0) };
|
||||
if parent_sid == -1 {
|
||||
anyhow::bail!("failed to read parent session id");
|
||||
}
|
||||
|
||||
let env_map: HashMap<String, String> = std::env::vars().collect();
|
||||
let script = "echo $$; sleep 0.2";
|
||||
let (program, args) = shell_command(script);
|
||||
let spawned = spawn_pipe_process(&program, &args, Path::new("."), &env_map, &None).await?;
|
||||
|
||||
let mut output_rx = spawned.output_rx;
|
||||
let pid_bytes =
|
||||
tokio::time::timeout(tokio::time::Duration::from_millis(500), output_rx.recv()).await??;
|
||||
let pid_text = String::from_utf8_lossy(&pid_bytes);
|
||||
let child_pid: i32 = pid_text
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing child pid output: {pid_text:?}"))?
|
||||
.parse()?;
|
||||
|
||||
let child_sid = unsafe { libc::getsid(child_pid) };
|
||||
if child_sid == -1 {
|
||||
anyhow::bail!("failed to read child session id");
|
||||
}
|
||||
|
||||
assert_eq!(child_sid, child_pid, "expected child to be session leader");
|
||||
assert_ne!(
|
||||
child_sid, parent_sid,
|
||||
"expected child to be detached from parent session"
|
||||
);
|
||||
|
||||
let exit_code = spawned.exit_rx.await.unwrap_or(-1);
|
||||
assert_eq!(
|
||||
exit_code, 0,
|
||||
"expected detached pipe process to exit cleanly"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn pipe_and_pty_share_interface() -> anyhow::Result<()> {
|
||||
let env_map: HashMap<String, String> = std::env::vars().collect();
|
||||
|
||||
Reference in New Issue
Block a user