dev: add pod runtime command override

This commit is contained in:
Keisuke Hirata 2026-06-01 05:37:17 +09:00
parent ca9e1840cf
commit 0031953ed3
No known key found for this signature in database
4 changed files with 114 additions and 8 deletions

View File

@ -3,6 +3,8 @@ use std::fmt;
use std::io; use std::io;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
const POD_RUNTIME_COMMAND_ENV: &str = "INSOMNIA_POD_RUNTIME_COMMAND";
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct PodRuntimeCommand { pub struct PodRuntimeCommand {
pub program: PathBuf, pub program: PathBuf,
@ -28,9 +30,29 @@ impl PodRuntimeCommand {
/// Resolve the Pod runtime command used for subprocess launches. /// Resolve the Pod runtime command used for subprocess launches.
/// ///
/// The default launch path is always the current `insomnia` executable plus /// 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> { pub fn resolve() -> io::Result<Self> {
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<F>(
override_program: Option<OsString>,
current_exe: F,
) -> io::Result<Self>
where
F: FnOnce() -> io::Result<PathBuf>,
{
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 { pub fn program(&self) -> &Path {
@ -98,4 +120,49 @@ mod tests {
.collect::<Vec<_>>() .collect::<Vec<_>>()
); );
} }
#[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::<Vec<_>>()
);
}
} }

View File

@ -53,7 +53,10 @@ pub enum SpawnError {
Io(io::Error), Io(io::Error),
/// runtime ディレクトリが解決できなかった (環境変数未設定等)。 /// runtime ディレクトリが解決できなかった (環境変数未設定等)。
RuntimeDirUnavailable, RuntimeDirUnavailable,
PodLaunchFailed(io::Error), PodLaunchFailed {
command: PodRuntimeCommand,
source: io::Error,
},
PodExitedEarly { PodExitedEarly {
stderr_tail: String, stderr_tail: String,
}, },
@ -68,7 +71,10 @@ impl std::fmt::Display for SpawnError {
f, f,
"could not resolve runtime directory (set INSOMNIA_HOME, INSOMNIA_RUNTIME_DIR, XDG_RUNTIME_DIR, or HOME)" "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 } => { Self::PodExitedEarly { stderr_tail } => {
if stderr_tail.is_empty() { if stderr_tail.is_empty() {
write!(f, "pod exited before becoming ready") 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<io::Error> for SpawnError { impl From<io::Error> for SpawnError {
fn from(e: io::Error) -> Self { fn from(e: io::Error) -> Self {
@ -132,7 +145,12 @@ where
.arg("--session-pod-name") .arg("--session-pod-name")
.arg(&config.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 // Default `kill_on_drop = false` plus `process_group(0)` makes this
// a detached Pod once startup succeeds: dropping the handle does not // a detached Pod once startup succeeds: dropping the handle does not

View File

@ -346,7 +346,13 @@ where
command.arg("--store").arg(store_dir); 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; let deadline = tokio::time::Instant::now() + RESTORE_START_TIMEOUT;
loop { loop {
if probe_socket(socket_path).await.reachable { if probe_socket(socket_path).await.reachable {
@ -545,6 +551,12 @@ pub enum PodDiscoveryError {
ScopeLock(#[from] pod_registry::ScopeLockError), ScopeLock(#[from] pod_registry::ScopeLockError),
#[error("failed to launch restore process: {0}")] #[error("failed to launch restore process: {0}")]
RestoreSpawn(io::Error), 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}")] #[error("restore process exited before socket became reachable: {status}")]
RestoreExited { status: std::process::ExitStatus }, RestoreExited { status: std::process::ExitStatus },
#[error("restore process did not become reachable before timeout")] #[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::PodStore(_)
| PodDiscoveryError::ScopeLock(_) | PodDiscoveryError::ScopeLock(_)
| PodDiscoveryError::RestoreSpawn(_) | PodDiscoveryError::RestoreSpawn(_)
| PodDiscoveryError::RestoreLaunchFailed { .. }
| PodDiscoveryError::RestoreExited { .. } | PodDiscoveryError::RestoreExited { .. }
| PodDiscoveryError::RestoreTimeout => ToolError::ExecutionFailed(error.to_string()), | PodDiscoveryError::RestoreTimeout => ToolError::ExecutionFailed(error.to_string()),
} }

View File

@ -2,7 +2,7 @@
INSOMNIA では、プロセス境界で本当に必要な場合を除き、環境変数の利用を避ける。新しい ambient な入力を増やすより、明示的な profile / manifest / config file / typed secret reference / CLI argument を優先する。 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 とも相性が悪い。 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 ## Build / example variables
これらは通常の application configuration ではない。 これらは通常の application configuration ではない。