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_in(cwd: &Path) -> Command {
187 IcpCli::new("icp", None).command_in(cwd)
188}
189
190pub fn add_target_args(
192 command: &mut Command,
193 environment: Option<&str>,
194 local_replica: Option<&LocalReplicaTarget>,
195) {
196 if let Some(environment) = environment {
197 if environment == LOCAL_ICP_TARGET
198 && let Some(local_replica) = local_replica
199 {
200 command.env_remove("ICP_ENVIRONMENT");
201 command
202 .arg("-n")
203 .arg(&local_replica.url)
204 .arg("-k")
205 .arg(&local_replica.root_key);
206 return;
207 }
208 command.args(["-e", environment]);
209 }
210}
211
212pub fn add_output_arg(command: &mut Command, output: &str) {
214 if output == "json" {
215 command.arg("--json");
216 } else {
217 command.args(["--output", output]);
218 }
219}
220
221pub(super) fn add_candid_arg(command: &mut Command, candid_path: Option<&Path>) {
223 if let Some(candid_path) = candid_path {
224 command.arg("--candid").arg(candid_path);
225 }
226}
227
228#[must_use]
230pub fn local_canister_candid_path(
231 icp_root: &Path,
232 artifact_environment: &str,
233 role: &str,
234) -> PathBuf {
235 artifact_root_path(icp_root, artifact_environment)
236 .join(role)
237 .join(format!("{role}.did"))
238}
239
240#[must_use]
242pub fn existing_local_canister_candid_path(
243 icp_root: &Path,
244 artifact_environment: &str,
245 role: &str,
246) -> Option<PathBuf> {
247 let path = local_canister_candid_path(icp_root, artifact_environment, role);
248 path.is_file().then_some(path)
249}
250
251pub(super) fn add_debug_arg(command: &mut Command, debug: bool) {
253 if debug {
254 command.arg("--debug");
255 }
256}
257
258pub fn ensure_command_compatible(command: &Command) -> Result<(), IcpCommandError> {
260 let executable = command.get_program().to_string_lossy();
261 compatible_version_output(executable.as_ref(), command.get_current_dir()).map(|_| ())
262}
263
264fn add_project_root_override_arg(command: &mut Command, cwd: &Path) {
265 command.arg("--project-root-override").arg(cwd);
266}
267
268#[must_use]
270pub fn command_display(command: &Command) -> String {
271 let mut parts = vec![command.get_program().to_string_lossy().to_string()];
272 parts.extend(
273 command
274 .get_args()
275 .map(|arg| arg.to_string_lossy().to_string()),
276 );
277 parts.join(" ")
278}