1 //
2 // Copyright 2023 Google, Inc.
3 //
4 // Licensed under the Apache License, Version 2.0 (the "License");
5 // you may not use this file except in compliance with the License.
6 // You may obtain a copy of the License at:
7 //
8 // http://www.apache.org/licenses/LICENSE-2.0
9 //
10 // Unless required by applicable law or agreed to in writing, software
11 // distributed under the License is distributed on an "AS IS" BASIS,
12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 // See the License for the specific language governing permissions and
14 // limitations under the License.
15
16 //! # os utility functions
17
18 use std::ffi::CString;
19 #[cfg(any(target_os = "linux", target_os = "macos"))]
20 use std::os::fd::AsRawFd;
21 #[cfg(target_os = "windows")]
22 use std::os::windows::io::AsRawHandle;
23
24 use std::{fs::remove_file, path::PathBuf};
25
26 use log::{error, info, warn};
27
28 use crate::system::netsimd_temp_dir;
29
30 use super::ini_file::IniFile;
31
32 const DEFAULT_HCI_PORT: u32 = 6402;
33
34 struct DiscoveryDir {
35 root_env: &'static str,
36 subdir: &'static str,
37 }
38
39 #[cfg(target_os = "linux")]
40 const DISCOVERY: DiscoveryDir = DiscoveryDir { root_env: "XDG_RUNTIME_DIR", subdir: "" };
41 #[cfg(target_os = "macos")]
42 const DISCOVERY: DiscoveryDir =
43 DiscoveryDir { root_env: "HOME", subdir: "Library/Caches/TemporaryItems" };
44 #[cfg(target_os = "windows")]
45 const DISCOVERY: DiscoveryDir = DiscoveryDir { root_env: "LOCALAPPDATA", subdir: "Temp" };
46 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
47 compile_error!("netsim only supports linux, Mac, and Windows");
48
49 /// Get discovery directory for netsim
get_discovery_directory() -> PathBuf50 pub fn get_discovery_directory() -> PathBuf {
51 // $TMPDIR is the temp directory on buildbots
52 if let Ok(test_env_p) = std::env::var("TMPDIR") {
53 return PathBuf::from(test_env_p);
54 }
55 let mut path = match std::env::var(DISCOVERY.root_env) {
56 Ok(env_p) => PathBuf::from(env_p),
57 Err(_) => {
58 warn!("No discovery env for {}, using /tmp", DISCOVERY.root_env);
59 PathBuf::from("/tmp")
60 }
61 };
62 path.push(DISCOVERY.subdir);
63 path
64 }
65
66 /// Get the filepath of netsim.ini under discovery directory
get_netsim_ini_filepath(instance_num: u16) -> PathBuf67 pub fn get_netsim_ini_filepath(instance_num: u16) -> PathBuf {
68 let mut discovery_dir = get_discovery_directory();
69 let filename = if instance_num == 1 {
70 "netsim.ini".to_string()
71 } else {
72 format!("netsim_{instance_num}.ini")
73 };
74 discovery_dir.push(filename);
75 discovery_dir
76 }
77
78 /// Remove the ini file
remove_netsim_ini(instance_num: u16)79 pub fn remove_netsim_ini(instance_num: u16) {
80 match remove_file(get_netsim_ini_filepath(instance_num)) {
81 Ok(_) => info!("Removed netsim ini file"),
82 Err(e) => error!("Failed to remove netsim ini file: {e:?}"),
83 }
84 }
85
86 /// Get the grpc server address for netsim
get_server_address(instance_num: u16) -> Option<String>87 pub fn get_server_address(instance_num: u16) -> Option<String> {
88 let filepath = get_netsim_ini_filepath(instance_num);
89 if !filepath.exists() {
90 error!("Unable to find netsim ini file: {filepath:?}");
91 return None;
92 }
93 if !filepath.is_file() {
94 error!("Not a file: {filepath:?}");
95 return None;
96 }
97 let mut ini_file = IniFile::new(filepath);
98 if let Err(err) = ini_file.read() {
99 error!("Error reading ini file: {err:?}");
100 }
101 ini_file.get("grpc.port").map(|s| {
102 if s.contains(':') {
103 s.to_string()
104 } else {
105 format!("localhost:{}", s)
106 }
107 })
108 }
109
110 const DEFAULT_INSTANCE: u16 = 1;
111
112 /// Get the netsim instance number which is always > 0
113 ///
114 /// The following priorities are used to determine the instance number:
115 ///
116 /// 1. The environment variable `NETSIM_INSTANCE`.
117 /// 2. The CLI flag `--instance`.
118 /// 3. The default value `DEFAULT_INSTANCE`.
get_instance(instance_flag: Option<u16>) -> u16119 pub fn get_instance(instance_flag: Option<u16>) -> u16 {
120 let instance_env: Option<u16> =
121 std::env::var("NETSIM_INSTANCE").ok().and_then(|i| i.parse().ok());
122 match (instance_env, instance_flag) {
123 (Some(i), _) if i > 0 => i,
124 (_, Some(i)) if i > 0 => i,
125 (_, _) => DEFAULT_INSTANCE,
126 }
127 }
128
129 /// Get the hci port number for netsim
get_hci_port(hci_port_flag: u32, instance: u16) -> u32130 pub fn get_hci_port(hci_port_flag: u32, instance: u16) -> u32 {
131 // The following priorities are used to determine the HCI port number:
132 //
133 // 1. The CLI flag `-hci_port`.
134 // 2. The environment variable `NETSIM_HCI_PORT`.
135 // 3. The default value `DEFAULT_HCI_PORT`
136 if hci_port_flag != 0 {
137 hci_port_flag
138 } else if let Ok(netsim_hci_port) = std::env::var("NETSIM_HCI_PORT") {
139 netsim_hci_port.parse::<u32>().unwrap()
140 } else {
141 DEFAULT_HCI_PORT + (instance as u32)
142 }
143 }
144
145 /// Get the netsim instance name used for log filename creation
get_instance_name(instance_num: Option<u16>, connector_instance: Option<u16>) -> String146 pub fn get_instance_name(instance_num: Option<u16>, connector_instance: Option<u16>) -> String {
147 let mut instance_name = String::new();
148 let instance = get_instance(instance_num);
149 if instance > 1 {
150 instance_name.push_str(&format!("{instance}_"));
151 }
152 // Note: This does not differentiate multiple connectors to the same instance.
153 if connector_instance.is_some() {
154 instance_name.push_str("connector_");
155 }
156 instance_name
157 }
158
159 /// Redirect Standard Stream
redirect_std_stream(instance_name: &str) -> anyhow::Result<()>160 pub fn redirect_std_stream(instance_name: &str) -> anyhow::Result<()> {
161 // Construct File Paths
162 let netsim_temp_dir = netsimd_temp_dir();
163 let stdout_filename = netsim_temp_dir
164 .join(format!("netsim_{instance_name}stdout.log"))
165 .into_os_string()
166 .into_string()
167 .map_err(|err| anyhow::anyhow!("{err:?}"))?;
168 let stderr_filename = netsim_temp_dir
169 .join(format!("netsim_{instance_name}stderr.log"))
170 .into_os_string()
171 .into_string()
172 .map_err(|err| anyhow::anyhow!("{err:?}"))?;
173
174 // CStrings
175 let stdout_filename_c = CString::new(stdout_filename)?;
176 let stderr_filename_c = CString::new(stderr_filename)?;
177 let mode_c = CString::new("w")?;
178
179 // Obtain the raw file descriptors for stdout.
180 #[cfg(any(target_os = "linux", target_os = "macos"))]
181 let stdout_fd = std::io::stdout().as_raw_fd();
182 #[cfg(target_os = "windows")]
183 // SAFETY: This operation allows opening a runtime file descriptor in Windows.
184 // This is necessary to translate the RawHandle as a FileDescriptor to redirect streams.
185 let stdout_fd =
186 unsafe { libc::open_osfhandle(std::io::stdout().as_raw_handle() as isize, libc::O_RDWR) };
187
188 // Obtain the raw file descriptors for stderr.
189 #[cfg(any(target_os = "linux", target_os = "macos"))]
190 let stderr_fd = std::io::stderr().as_raw_fd();
191 #[cfg(target_os = "windows")]
192 // SAFETY: This operation allows opening a runtime file descriptor in Windows.
193 // This is necessary to translate the RawHandle as a FileDescriptor to redirect streams.
194 let stderr_fd =
195 unsafe { libc::open_osfhandle(std::io::stderr().as_raw_handle() as isize, libc::O_RDWR) };
196
197 // SAFETY: These operations allow redirection of stdout and stderr stream to a file if terminal.
198 // Convert the raw file descriptors to FILE pointers using libc::fdopen.
199 // This is necessary because freopen expects a FILE* as its last argument, not a raw file descriptor.
200 // Use freopen to redirect stdout and stderr to the specified files.
201 unsafe {
202 let stdout_file = libc::fdopen(stdout_fd, mode_c.as_ptr());
203 let stderr_file = libc::fdopen(stderr_fd, mode_c.as_ptr());
204 libc::freopen(stdout_filename_c.as_ptr(), mode_c.as_ptr(), stdout_file);
205 libc::freopen(stderr_filename_c.as_ptr(), mode_c.as_ptr(), stderr_file);
206 }
207
208 Ok(())
209 }
210
211 #[cfg(test)]
212 mod tests {
213
214 use super::*;
215 #[cfg(not(target_os = "windows"))]
216 use crate::system::tests::ENV_MUTEX;
217
218 #[test]
test_get_discovery_directory()219 fn test_get_discovery_directory() {
220 #[cfg(not(target_os = "windows"))]
221 let _locked = ENV_MUTEX.lock();
222 // Remove all environment variable
223 std::env::remove_var(DISCOVERY.root_env);
224 std::env::remove_var("TMPDIR");
225
226 // Test with no environment variables
227 let actual = get_discovery_directory();
228 let mut expected = PathBuf::from("/tmp");
229 expected.push(DISCOVERY.subdir);
230 assert_eq!(actual, expected);
231
232 // Test with root_env variable
233 std::env::set_var(DISCOVERY.root_env, "/netsim-test");
234 let actual = get_discovery_directory();
235 let mut expected = PathBuf::from("/netsim-test");
236 expected.push(DISCOVERY.subdir);
237 assert_eq!(actual, expected);
238
239 // Test with TMPDIR variable
240 std::env::set_var("TMPDIR", "/tmpdir");
241 assert_eq!(get_discovery_directory(), PathBuf::from("/tmpdir"));
242
243 // Test get_netsim_ini_filepath
244 assert_eq!(get_netsim_ini_filepath(1), PathBuf::from("/tmpdir/netsim.ini"));
245 assert_eq!(get_netsim_ini_filepath(2), PathBuf::from("/tmpdir/netsim_2.ini"));
246 }
247
248 #[test]
test_get_instance_and_instance_name()249 fn test_get_instance_and_instance_name() {
250 // Set NETSIM_INSTANCE environment variable
251 std::env::set_var("NETSIM_INSTANCE", "100");
252 assert_eq!(get_instance(Some(0)), 100);
253 assert_eq!(get_instance(Some(1)), 100);
254
255 // Remove NETSIM_INSTANCE environment variable
256 std::env::remove_var("NETSIM_INSTANCE");
257 assert_eq!(get_instance(None), DEFAULT_INSTANCE);
258 assert_eq!(get_instance(Some(0)), DEFAULT_INSTANCE);
259 assert_eq!(get_instance(Some(1)), 1);
260
261 // Default cases - instance name should be empty string
262 assert_eq!(get_instance_name(None, None), "");
263 assert_eq!(get_instance_name(Some(1), None), "");
264
265 // Default instance but connector set - Expect instance name to be "connector_"
266 assert_eq!(get_instance_name(None, Some(3)), "connector_");
267 assert_eq!(get_instance_name(Some(1), Some(1)), "connector_");
268 assert_eq!(get_instance_name(Some(1), Some(2)), "connector_");
269
270 // Both instance and connector set - Expect instance name to be "<instance>_connector_"
271 assert_eq!(get_instance_name(Some(2), Some(1)), "2_connector_");
272 assert_eq!(get_instance_name(Some(3), Some(3)), "3_connector_");
273 }
274
275 #[test]
test_get_hci_port()276 fn test_get_hci_port() {
277 // Test if hci_port flag exists
278 assert_eq!(get_hci_port(1, u16::MAX), 1);
279 assert_eq!(get_hci_port(1, u16::MIN), 1);
280
281 // Remove NETSIM_HCI_PORT with hci_port_flag = 0
282 std::env::remove_var("NETSIM_HCI_PORT");
283 assert_eq!(get_hci_port(0, 0), DEFAULT_HCI_PORT);
284 assert_eq!(get_hci_port(0, 1), DEFAULT_HCI_PORT + 1);
285
286 // Set NETSIM_HCI_PORT
287 std::env::set_var("NETSIM_HCI_PORT", "100");
288 assert_eq!(get_hci_port(0, 0), 100);
289 assert_eq!(get_hci_port(0, u16::MAX), 100);
290 }
291 }
292