canic_host/icp/
command.rs1use std::{
2 io,
3 os::fd::BorrowedFd,
4 path::{Path, PathBuf},
5 process::Command,
6};
7
8#[cfg(unix)]
9use std::os::unix::process::CommandExt;
10
11#[cfg(unix)]
12use rustix::io::{FdFlags, fcntl_getfd, fcntl_setfd};
13
14use crate::release_set::artifact_root_path;
15
16use super::{
17 error::IcpCommandError,
18 model::{IcpCli, LOCAL_ICP_TARGET, LocalReplicaTarget},
19 version::compatible_version_output,
20};
21
22impl IcpCli {
23 #[must_use]
25 pub fn new(executable: impl Into<String>, environment: Option<String>) -> Self {
26 Self {
27 executable: executable.into(),
28 environment,
29 cwd: None,
30 local_replica: None,
31 inherited_fd: None,
32 }
33 }
34
35 #[must_use]
37 pub fn with_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
38 self.cwd = Some(cwd.into());
39 self
40 }
41
42 #[must_use]
44 pub fn with_local_replica(mut self, target: Option<LocalReplicaTarget>) -> Self {
45 self.local_replica = target;
46 self
47 }
48
49 #[must_use]
51 pub const fn with_inherited_fd(mut self, inherited_fd: Option<i32>) -> Self {
52 self.inherited_fd = inherited_fd;
53 self
54 }
55
56 #[must_use]
58 pub fn environment(&self) -> Option<&str> {
59 self.environment.as_deref()
60 }
61
62 #[must_use]
64 pub fn command(&self) -> Command {
65 let mut command = Command::new(&self.executable);
66 if let Some(cwd) = &self.cwd {
67 command.current_dir(cwd);
68 add_project_root_override_arg(&mut command, cwd);
69 }
70 configure_inherited_fd(&mut command, self.inherited_fd);
71 command
72 }
73
74 #[must_use]
76 pub fn command_in(&self, cwd: &Path) -> Command {
77 let mut command = Command::new(&self.executable);
78 command.current_dir(cwd);
79 add_project_root_override_arg(&mut command, cwd);
80 configure_inherited_fd(&mut command, self.inherited_fd);
81 command
82 }
83
84 #[must_use]
86 pub fn canister_command(&self) -> Command {
87 let mut command = self.command();
88 command.arg("canister");
89 command
90 }
91
92 pub(super) fn add_target_args(&self, command: &mut Command) {
93 add_target_args(command, self.environment(), self.local_replica.as_ref());
94 }
95}
96
97pub(super) fn configure_inherited_fd(command: &mut Command, inherited_fd: Option<i32>) {
98 let Some(inherited_fd) = inherited_fd else {
99 return;
100 };
101
102 #[cfg(unix)]
103 unsafe {
107 command.pre_exec(move || {
108 let fd = BorrowedFd::borrow_raw(inherited_fd);
111 let mut flags = fcntl_getfd(fd).map_err(errno_to_io)?;
112 flags.remove(FdFlags::CLOEXEC);
113 fcntl_setfd(fd, flags).map_err(errno_to_io)
114 });
115 }
116
117 #[cfg(not(unix))]
118 let _ = (command, inherited_fd);
119}
120
121#[cfg(unix)]
122fn errno_to_io(error: rustix::io::Errno) -> io::Error {
123 io::Error::from_raw_os_error(error.raw_os_error())
124}
125
126#[cfg(all(test, unix))]
127mod tests {
128 use super::*;
129 use crate::test_support::temp_dir;
130 use rustix::fs::{FlockOperation, flock};
131 use std::{fs, time::Duration};
132
133 #[test]
134 fn configured_descriptor_lives_through_command_descendants_only() {
135 let root = temp_dir("canic-icp-command-inherited-fd");
136 fs::create_dir_all(&root).expect("create temp root");
137 let lock_path = root.join("command.lock");
138 let owner = fs::OpenOptions::new()
139 .read(true)
140 .write(true)
141 .create(true)
142 .truncate(false)
143 .open(&lock_path)
144 .expect("open owner lock");
145 flock(&owner, FlockOperation::NonBlockingLockExclusive).expect("lock owner file");
146
147 let mut command = Command::new("sh");
148 command.args(["-c", "sleep 0.2 & wait"]);
149 configure_inherited_fd(&mut command, Some(std::os::fd::AsRawFd::as_raw_fd(&owner)));
150 let mut child = command.spawn().expect("spawn inherited command tree");
151 drop(owner);
152
153 let contender = fs::OpenOptions::new()
154 .read(true)
155 .write(true)
156 .open(&lock_path)
157 .expect("open contender lock");
158 assert_eq!(
159 flock(&contender, FlockOperation::NonBlockingLockExclusive),
160 Err(rustix::io::Errno::WOULDBLOCK)
161 );
162
163 child.wait().expect("wait for command tree");
164 for _ in 0..50 {
165 match flock(&contender, FlockOperation::NonBlockingLockExclusive) {
166 Ok(()) => {
167 fs::remove_dir_all(root).expect("remove temp root");
168 return;
169 }
170 Err(rustix::io::Errno::WOULDBLOCK) => {
171 std::thread::sleep(Duration::from_millis(5));
172 }
173 Err(error) => panic!("unexpected contender lock error: {error}"),
174 }
175 }
176 panic!("inherited descriptor survived the command tree");
177 }
178}
179
180pub(super) fn add_local_network_target(command: &mut Command) {
181 command.arg(LOCAL_ICP_TARGET);
182}
183
184#[must_use]
186pub fn default_command() -> Command {
187 IcpCli::new("icp", None).command()
188}
189
190#[must_use]
192pub fn default_command_in(cwd: &Path) -> Command {
193 IcpCli::new("icp", None).command_in(cwd)
194}
195
196pub fn add_target_args(
198 command: &mut Command,
199 environment: Option<&str>,
200 local_replica: Option<&LocalReplicaTarget>,
201) {
202 if let Some(environment) = environment {
203 if environment == LOCAL_ICP_TARGET
204 && let Some(local_replica) = local_replica
205 {
206 command.env_remove("ICP_ENVIRONMENT");
207 command
208 .arg("-n")
209 .arg(&local_replica.url)
210 .arg("-k")
211 .arg(&local_replica.root_key);
212 return;
213 }
214 command.args(["-e", environment]);
215 }
216}
217
218pub fn add_output_arg(command: &mut Command, output: &str) {
220 if output == "json" {
221 command.arg("--json");
222 } else {
223 command.args(["--output", output]);
224 }
225}
226
227pub fn add_candid_arg(command: &mut Command, candid_path: Option<&Path>) {
229 if let Some(candid_path) = candid_path {
230 command.arg("--candid").arg(candid_path);
231 }
232}
233
234#[must_use]
236pub fn local_canister_candid_path(
237 icp_root: &Path,
238 artifact_environment: &str,
239 role: &str,
240) -> PathBuf {
241 artifact_root_path(icp_root, artifact_environment)
242 .join(role)
243 .join(format!("{role}.did"))
244}
245
246#[must_use]
248pub fn existing_local_canister_candid_path(
249 icp_root: &Path,
250 artifact_environment: &str,
251 role: &str,
252) -> Option<PathBuf> {
253 let path = local_canister_candid_path(icp_root, artifact_environment, role);
254 path.is_file().then_some(path)
255}
256
257pub fn add_debug_arg(command: &mut Command, debug: bool) {
259 if debug {
260 command.arg("--debug");
261 }
262}
263
264pub fn ensure_command_compatible(command: &Command) -> Result<(), IcpCommandError> {
266 let executable = command.get_program().to_string_lossy();
267 compatible_version_output(executable.as_ref(), command.get_current_dir()).map(|_| ())
268}
269
270fn add_project_root_override_arg(command: &mut Command, cwd: &Path) {
271 command.arg("--project-root-override").arg(cwd);
272}
273
274#[must_use]
276pub fn command_display(command: &Command) -> String {
277 let mut parts = vec![command.get_program().to_string_lossy().to_string()];
278 parts.extend(
279 command
280 .get_args()
281 .map(|arg| arg.to_string_lossy().to_string()),
282 );
283 parts.join(" ")
284}