diff --git a/crates/client/src/runtime_command.rs b/crates/client/src/runtime_command.rs index 977691cc..a67a620a 100644 --- a/crates/client/src/runtime_command.rs +++ b/crates/client/src/runtime_command.rs @@ -3,6 +3,8 @@ use std::fmt; use std::io; use std::path::{Path, PathBuf}; +const POD_RUNTIME_COMMAND_ENV: &str = "INSOMNIA_POD_RUNTIME_COMMAND"; + #[derive(Clone, Debug, PartialEq, Eq)] pub struct PodRuntimeCommand { pub program: PathBuf, @@ -28,9 +30,29 @@ impl PodRuntimeCommand { /// Resolve the Pod runtime command used for subprocess launches. /// /// The default launch path is always the current `insomnia` executable plus - /// the unified `pod` prefix argument. + /// the unified `pod` prefix argument. During development, a non-empty + /// `INSOMNIA_POD_RUNTIME_COMMAND` value replaces only the executable path; + /// the `pod` prefix is still added here and the env value is not parsed as a + /// shell command. pub fn resolve() -> io::Result { - Self::for_current_exe() + Self::resolve_from_env_value( + std::env::var_os(POD_RUNTIME_COMMAND_ENV), + std::env::current_exe, + ) + } + + fn resolve_from_env_value( + override_program: Option, + current_exe: F, + ) -> io::Result + where + F: FnOnce() -> io::Result, + { + if let Some(program) = override_program.filter(|program| !program.as_os_str().is_empty()) { + return Ok(Self::for_executable(program)); + } + + Ok(Self::for_executable(current_exe()?)) } pub fn program(&self) -> &Path { @@ -98,4 +120,49 @@ mod tests { .collect::>() ); } + + #[test] + fn resolve_uses_current_exe_when_override_is_unset() { + let command = PodRuntimeCommand::resolve_from_env_value(None, || { + Ok(PathBuf::from("/opt/insomnia/bin/insomnia")) + }) + .unwrap(); + + assert_eq!( + command, + PodRuntimeCommand::for_executable("/opt/insomnia/bin/insomnia") + ); + } + + #[test] + fn resolve_uses_current_exe_when_override_is_empty() { + let command = PodRuntimeCommand::resolve_from_env_value(Some(OsString::new()), || { + Ok(PathBuf::from("/opt/insomnia/bin/insomnia")) + }) + .unwrap(); + + assert_eq!( + command, + PodRuntimeCommand::for_executable("/opt/insomnia/bin/insomnia") + ); + } + + #[test] + fn resolve_override_replaces_only_program_and_keeps_pod_prefix() { + let command = PodRuntimeCommand::resolve_from_env_value( + Some(OsString::from("/tmp/rebuilt insomnia")), + || panic!("override must not inspect current_exe"), + ) + .unwrap(); + + assert_eq!(command.program(), Path::new("/tmp/rebuilt insomnia")); + assert_eq!(command.prefix_args(), [OsString::from("pod")]); + assert_eq!( + command.argv_with(["--pod", "agent"]), + vec!["pod", "--pod", "agent"] + .into_iter() + .map(OsString::from) + .collect::>() + ); + } } diff --git a/crates/client/src/spawn.rs b/crates/client/src/spawn.rs index d20bfbf9..21bd4536 100644 --- a/crates/client/src/spawn.rs +++ b/crates/client/src/spawn.rs @@ -53,7 +53,10 @@ pub enum SpawnError { Io(io::Error), /// runtime ディレクトリが解決できなかった (環境変数未設定等)。 RuntimeDirUnavailable, - PodLaunchFailed(io::Error), + PodLaunchFailed { + command: PodRuntimeCommand, + source: io::Error, + }, PodExitedEarly { stderr_tail: String, }, @@ -68,7 +71,10 @@ impl std::fmt::Display for SpawnError { f, "could not resolve runtime directory (set INSOMNIA_HOME, INSOMNIA_RUNTIME_DIR, XDG_RUNTIME_DIR, or HOME)" ), - Self::PodLaunchFailed(e) => write!(f, "failed to launch pod: {e}"), + Self::PodLaunchFailed { command, source } => write!( + f, + "failed to launch pod runtime command `{command}`: {source}" + ), Self::PodExitedEarly { stderr_tail } => { if stderr_tail.is_empty() { write!(f, "pod exited before becoming ready") @@ -85,7 +91,14 @@ impl std::fmt::Display for SpawnError { } } -impl std::error::Error for SpawnError {} +impl std::error::Error for SpawnError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Io(error) | Self::PodLaunchFailed { source: error, .. } => Some(error), + Self::RuntimeDirUnavailable | Self::PodExitedEarly { .. } | Self::Timeout => None, + } + } +} impl From for SpawnError { fn from(e: io::Error) -> Self { @@ -132,7 +145,12 @@ where .arg("--session-pod-name") .arg(&config.pod_name); } - let mut child = command.spawn().map_err(SpawnError::PodLaunchFailed)?; + let mut child = command + .spawn() + .map_err(|source| SpawnError::PodLaunchFailed { + command: config.runtime_command.clone(), + source, + })?; // Default `kill_on_drop = false` plus `process_group(0)` makes this // a detached Pod once startup succeeds: dropping the handle does not diff --git a/crates/pod/src/discovery.rs b/crates/pod/src/discovery.rs index c0e6ae46..3f412f2a 100644 --- a/crates/pod/src/discovery.rs +++ b/crates/pod/src/discovery.rs @@ -346,7 +346,13 @@ where command.arg("--store").arg(store_dir); } - let mut child = command.spawn().map_err(PodDiscoveryError::RestoreSpawn)?; + let mut child = + command + .spawn() + .map_err(|source| PodDiscoveryError::RestoreLaunchFailed { + command: runtime_command.clone(), + source, + })?; let deadline = tokio::time::Instant::now() + RESTORE_START_TIMEOUT; loop { if probe_socket(socket_path).await.reachable { @@ -545,6 +551,12 @@ pub enum PodDiscoveryError { ScopeLock(#[from] pod_registry::ScopeLockError), #[error("failed to launch restore process: {0}")] RestoreSpawn(io::Error), + #[error("failed to launch restore runtime command `{command}`: {source}")] + RestoreLaunchFailed { + command: PodRuntimeCommand, + #[source] + source: io::Error, + }, #[error("restore process exited before socket became reachable: {status}")] RestoreExited { status: std::process::ExitStatus }, #[error("restore process did not become reachable before timeout")] @@ -779,6 +791,7 @@ fn discovery_error_to_tool_error(error: PodDiscoveryError) -> ToolError { | PodDiscoveryError::PodStore(_) | PodDiscoveryError::ScopeLock(_) | PodDiscoveryError::RestoreSpawn(_) + | PodDiscoveryError::RestoreLaunchFailed { .. } | PodDiscoveryError::RestoreExited { .. } | PodDiscoveryError::RestoreTimeout => ToolError::ExecutionFailed(error.to_string()), } diff --git a/docs/environment.md b/docs/environment.md index 14f69c83..592ffded 100644 --- a/docs/environment.md +++ b/docs/environment.md @@ -2,7 +2,7 @@ INSOMNIA では、プロセス境界で本当に必要な場合を除き、環境変数の利用を避ける。新しい ambient な入力を増やすより、明示的な profile / manifest / config file / typed secret reference / CLI argument を優先する。 -それでも、path discovery、runtime directory、外部 provider の credential 慣習との移行互換のために、一部の環境変数はまだサポートしている。この文書に載せた環境変数は公開 surface として扱う。ただし、fallback 変数は独立した設定項目ではなく、対応する main key の解決順の一部として扱う。開発・テスト都合だけの環境変数は追加しない。 +それでも、path discovery、runtime directory、外部 provider の credential 慣習との移行互換のために、一部の環境変数はまだサポートしている。この文書に載せた通常 runtime 用の環境変数は公開 surface として扱う。ただし、fallback 変数は独立した設定項目ではなく、対応する main key の解決順の一部として扱う。開発・テスト都合だけの環境変数は、通常ユーザー向け configuration として扱わない明確な escape hatch に限る。 ## 原則 @@ -56,6 +56,14 @@ Provider credential は、現在は manifest / profile / catalog の設定から Credential env var は interoperability のために現時点では残っているが、長期的に望ましい secret mechanism ではない。現時点では適切なら `auth.file` を優先し、今後は typed secret reference へ寄せる。credential UX のために implicit `.env` loading を追加しないこと。project secret を漏らしやすく、profile ごとの credential model とも相性が悪い。 +## Development-only escape hatches + +これらは dogfooding / self-rebuild / fixture などの開発運用だけの逃げ道であり、通常ユーザー向けの configuration surface ではない。profile、manifest、CLI option の代替として案内しない。 + +| 変数 | Context | 備考 | +| --- | --- | --- | +| `INSOMNIA_POD_RUNTIME_COMMAND` | 開発中に起動中の `insomnia` binary が rebuild され、`std::env::current_exe()` が `target/debug/insomnia (deleted)` のような stale path を返す場合の Pod runtime executable override。 | Unset または empty の場合は既定どおり current executable に `pod` prefix argument を付けて起動する。Non-empty の場合は値を executable path としてそのまま使い、`pod` prefix argument は常に自動追加する。shell parsing や argument splitting は行わないため、値に flags や `pod` を含めない。 | + ## Build / example variables これらは通常の application configuration ではない。