xref: /aosp_15_r20/tools/netsim/rust/common/src/util/os_utils.rs (revision cf78ab8cffb8fc9207af348f23af247fb04370a6)
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