blob: 52800e42dd1d5e4b1ddb311a8bac34b06d9ae55b [file] [log] [blame]
// Copyright lowRISC contributors.
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
// SPDX-License-Identifier: Apache-2.0
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::ffi::OsString;
use std::fs;
use std::fs::File;
use std::io::BufReader;
use std::io::{ErrorKind, Read};
use std::os::unix::fs::FileTypeExt;
use std::os::unix::net::UnixListener;
use std::path::{Path, PathBuf};
use std::process::{Child, Command};
use std::rc::Rc;
use std::thread;
use std::time::Duration;
use anyhow::{bail, Context, Result};
use log;
use nix::sys::signal::{self, Signal};
use nix::unistd::Pid;
use serde::{Deserialize, Serialize};
use crate::io::emu::{EmuError, EmuState, EmuValue, Emulator};
use crate::transport::ti50emulator::gpio::GpioConfiguration;
use crate::transport::ti50emulator::Inner;
use crate::transport::ti50emulator::Ti50Emulator;
const TIMEOUT: Duration = Duration::from_millis(1000);
const MAX_RETRY: usize = 5;
const PATTERN: &[u8; 5] = b"READY";
pub const EMULATOR_INVALID_ID: u64 = 0;
#[derive(Serialize, Deserialize)]
pub struct EmulatorConfig {
pub gpio: HashMap<String, GpioConfiguration>,
pub uart: HashMap<String, String>,
pub i2c: HashMap<String, String>,
}
pub struct EmulatorProcess {
/// Current working directory for Emulator sub-process.
runtime_directory: PathBuf,
/// Directory with contain TockOS Applications and Kernel.
executable_directory: PathBuf,
/// Default name of TockOS kernel file.
executable: String,
/// Map of currently used argument by kernel.
current_args: HashMap<String, EmuValue>,
/// Current state of Emulator.
state: EmuState,
/// Handle to Emulator sub-proccess.
proc: Option<Child>,
/// Counter of 'power' cycle
power_cycle_count: u32,
}
impl EmulatorProcess {
/// Create new instance of [`EmulatorProcess`] based on provided parameters.
pub fn init(
instance_directory: &Path,
executable_directory: &Path,
executable: &str,
) -> Result<Self> {
let runtime_directory = instance_directory.join("runtime");
fs::create_dir(&runtime_directory).context("Failed to create runtime directory")?;
Ok(Self {
executable_directory: executable_directory.to_owned(),
executable: executable.to_owned(),
runtime_directory,
current_args: HashMap::from([(
String::from("exec"),
EmuValue::String(String::from(executable)),
)]),
state: EmuState::Off,
proc: None,
power_cycle_count: 1,
})
}
pub fn get_state(&self) -> EmuState {
self.state
}
pub fn get_runtime_dir(&self) -> &Path {
&self.runtime_directory
}
pub fn get_id(&self) -> u64 {
if let Some(proc) = &self.proc {
((self.power_cycle_count as u64) << 32) + (proc.id() as u64)
} else {
EMULATOR_INVALID_ID
}
}
pub fn get_configurations(&self) -> Result<EmulatorConfig> {
let args_list = vec![
OsString::from("--path"),
self.runtime_directory.clone().into_os_string(),
OsString::from("--gen_configs"),
];
let exec: PathBuf = match self.current_args.get("exec") {
Some(EmuValue::String(exec_name)) => self.executable_directory.join(exec_name),
_ => {
bail!(EmuError::RuntimeError(
"Can't get configurations invalid executable".to_string()
))
}
};
log::info!("Ti50Emulator getting configuration ");
log::info!("Command: {} {:?}", exec.display(), args_list);
let status = Command::new(&exec)
.args(args_list)
.status()
.context("Could not spawn sub-process")?;
if status.success() {
log::info!("Ti50Emulator parsing configurations");
let file = File::open(self.runtime_directory.join("he_conf.json"))
.context("Configuration file open error")?;
let reader = BufReader::new(file);
let config: EmulatorConfig =
serde_json::from_reader(reader).context("Configuration parsing error")?;
Ok(config)
} else {
bail!(EmuError::RuntimeError(format!(
"Ti50Emulator sub-process exit with error: {}",
status
)));
}
}
/// Updates `state` based on sub-process exit status and current value of `state`.
pub fn update_status(&mut self) -> Result<()> {
if let Some(proc) = &mut self.proc {
match proc.try_wait() {
Ok(Some(status)) => {
if status.success() {
log::info!("Ti50Emulator exit with status {}", status);
self.state = EmuState::Off;
} else if self.state != EmuState::Error {
log::info!("Ti50Emulator sub-process exit with error: {}", status);
self.state = EmuState::Error;
}
self.power_cycle_count += 1;
self.proc = None;
}
Ok(None) => {
self.state = EmuState::On;
}
Err(err) => {
bail!(EmuError::RuntimeError(format!(
"Can't aquire status from sub-process pid:{} error:{}",
proc.id(),
err
)));
}
}
} else if self.state == EmuState::On {
self.state = EmuState::Error;
bail!(EmuError::RuntimeError(
"Non sub-process found but state indicate that Emulator is ON".to_string()
));
}
Ok(())
}
/// Run Emulator executable as sub-process and wait until Emulator is ready to work.
fn spawn_process(&mut self) -> Result<()> {
let socket_path = self.runtime_directory.join("control_soc");
let mut args_list = vec![
OsString::from("--path"),
self.runtime_directory.clone().into_os_string(),
OsString::from("--control_socket"),
socket_path.clone().into_os_string(),
];
match self.current_args.get("apps") {
Some(EmuValue::StringList(apps)) => {
args_list.push(OsString::from("--apps"));
args_list.extend(
apps.iter()
.map(|a| self.executable_directory.join(a).into()),
);
}
None => {
bail!(EmuError::StartFailureCause(
"Ti50 sub-process missing application list".to_string()
))
}
_ => {
bail!(EmuError::StartFailureCause(
"Ti50 sub-process expect apps to be list of string".to_string()
));
}
}
let exec = match self.current_args.get("exec") {
Some(EmuValue::String(exec_name)) => self.executable_directory.join(exec_name),
None => {
bail!(EmuError::StartFailureCause(
"Ti50 sub-process invalid executable".to_string()
))
}
_ => {
bail!(EmuError::StartFailureCause(
"Ti50 sub-process except exec name to be string".to_string()
))
}
};
log::info!("Waiting for sub-process start");
let ready_handle = thread::spawn(move || {
let control_socket = UnixListener::bind(socket_path).unwrap();
control_socket
.set_nonblocking(true)
.expect("Can't set non-blocking socket");
let mut buffer = [0u8; 8];
let mut retry = 0;
for stream in control_socket.incoming() {
match stream {
Ok(mut socket) => {
let len = socket.read(&mut buffer[..]).unwrap();
if PATTERN[..] == buffer[0..PATTERN.len()] {
log::info!("Ti50Emulator ready");
}
return Ok(len);
}
Err(err) if err.kind() == ErrorKind::WouldBlock => {
log::debug!("Wait for sub-process...");
std::thread::sleep(TIMEOUT);
retry += 1;
if retry >= MAX_RETRY {
return Err(EmuError::StartFailureCause(
"Spawning Ti50Emulator sub-process timeout".to_string(),
));
}
}
Err(_err) => {
return Err(EmuError::RuntimeError(
"Control socket io error".to_string(),
));
}
}
}
Err(EmuError::StartFailureCause(
"Waiting for sub-process failed".to_string(),
))
});
log::info!("Spawning Ti50Emulator sub-process");
log::info!("Command: {} {:?}", exec.display(), args_list);
let handle = Command::new(&exec)
.args(args_list)
.spawn()
.context("Could not spawn sub-process")?;
self.proc = Some(handle);
match ready_handle
.join()
.expect("Can't join control socket thread")
{
Ok(_) => Ok(()),
Err(err) => Err(err.into()),
}
}
/// The function tries to safely terminate the Emulator sub-process.
/// If the sub-process does not finish its execution after time
/// described in `TIMEOUT` * `MAX_RETRY`, use SIGKILL to force sub-process termination.
/// If all method fail, it returns an EmuError.
fn stop_process(&mut self) -> Result<()> {
self.power_cycle_count += 1;
if let Some(handle) = &mut self.proc {
let pid = handle.id() as i32;
log::debug!("Stop sub-process PID:{} SIGTERM", pid);
signal::kill(Pid::from_raw(pid), Signal::SIGTERM)
.context("Stop sub-process using SIGTERM")?;
for _retry in 0..MAX_RETRY {
log::debug!("Stop sub-process PID:{} ...", pid);
match handle.try_wait() {
Ok(None) => {}
Ok(Some(status)) => {
log::info!("Stop sub-process terminated PID: {} {}", pid, status);
self.cleanup()?;
self.state = EmuState::Off;
self.proc = None;
return Ok(());
}
Err(e) => {
self.state = EmuState::Error;
bail!(EmuError::StopFailureCause(format!(
"Unexpected error querying process presence: {}",
e
)));
}
}
std::thread::sleep(TIMEOUT);
}
log::debug!("Stop sub-process PID:{} SIGKILL", pid);
for _retry in 0..MAX_RETRY {
match signal::kill(Pid::from_raw(pid), Signal::SIGKILL) {
Ok(()) => {}
Err(nix::Error::Sys(nix::errno::Errno::ESRCH)) => {
log::debug!("Stop sub-process PID:{} process terminated", pid);
self.cleanup()?;
self.proc = None;
self.state = EmuState::Off;
return Ok(());
}
Err(e) => {
self.proc = None;
self.state = EmuState::Error;
bail!(EmuError::StopFailureCause(format!(
"Unable to stop process pid:{} error:{}",
pid, e
)));
}
}
std::thread::sleep(TIMEOUT);
}
self.state = EmuState::Error;
return Err(EmuError::StopFailureCause(format!(
"Timeout unable to stop process pid:{}",
pid,
))
.into());
} else if self.state == EmuState::Error {
log::warn!("Stop sub-process don't exist clean error state");
self.cleanup()?;
self.state = EmuState::Off;
}
Ok(())
}
/// Method remove all peripheral files placed in the runtime directory.
fn cleanup(&mut self) -> Result<()> {
log::debug!("Cleanup runtime directory");
for file in fs::read_dir(&self.runtime_directory)? {
let path = file.unwrap().path();
let meta = fs::metadata(&path)?;
let file_type = meta.file_type();
if file_type.is_socket() || file_type.is_fifo() {
fs::remove_file(&path)?;
}
}
Ok(())
}
/// Method reset all internal states of Emulator to its default values.
fn reset_state(&mut self) -> Result<()> {
fs::remove_dir_all(&self.runtime_directory)?;
fs::create_dir(&self.runtime_directory)?;
self.current_args.clear();
self.current_args.insert(
String::from("exec"),
EmuValue::String(self.executable.clone()),
);
Ok(())
}
/// Update content of resource with data from `source`, overwrite file if it already exist.
fn update_resource(&self, key: &str, source: &Path) -> Result<()> {
let name = source.file_name().ok_or_else(|| {
EmuError::InvalidArgumentValue(String::from(key), source.display().to_string())
})?;
let destination = self.runtime_directory.join(name);
log::debug!(
"Update resource:{:?} with data from path: {:?}",
key,
source,
);
fs::copy(source, &destination).with_context(|| {
format!(
"Failed to copy resource file: {} to runtime directory: {}",
&source.display(),
&destination.display()
)
})?;
Ok(())
}
/// Method update state files and parameters passed to Emulator sub-process.
/// If `factory_reset` is set to true old resource files stored in runtime_directory will be removed and
/// current_args will be set to its default value.
/// Values from parameter `args` is used to update value of current argument set passed to
/// sub-process. If `args` contains paths to files they will be copied to the runtime directory.
fn update_args(&mut self, factory_reset: bool, args: &HashMap<String, EmuValue>) -> Result<()> {
let allowed = HashSet::from(["exec", "flash", "apps", "version_state", "pmu_state"]);
let mandatory = ["exec", "apps"];
for &name in mandatory.iter() {
if !self.current_args.contains_key(name) && !args.contains_key(name) {
bail!(EmuError::StartFailureCause(format!(
"Missing argument {}",
name
)))
}
}
if factory_reset {
self.reset_state()?;
}
for (key, item) in args.iter() {
if allowed.contains(key.as_str()) {
match item {
EmuValue::FilePath(path) => {
self.update_resource(key, path)?;
}
EmuValue::FilePathList(path_list) => {
for path in path_list.iter() {
self.update_resource(key, path)?;
}
}
_ => {}
}
self.current_args.insert(key.clone(), item.clone());
continue;
}
bail!(EmuError::InvalidArgumetName(key.clone()));
}
Ok(())
}
}
/// Structure representing `Emulator` sub-process based on TockOS host-emulation architecture.
pub struct Ti50SubProcess {
inner: Rc<RefCell<Inner>>,
}
impl Ti50SubProcess {
/// Create a new `Ti50SubProcess` instance.
pub fn open(ti50: &Ti50Emulator) -> Result<Self> {
Ok(Self {
inner: ti50.inner.clone(),
})
}
}
impl Emulator for Ti50SubProcess {
/// Simple function with return `EmuState` representing current state of Emulator instance.
fn get_state(&self) -> Result<EmuState> {
let process = &mut self.inner.borrow_mut().process;
process.update_status()?;
Ok(process.state)
}
/// Start emulator sub-process with provided arguments.
fn start(&self, factory_reset: bool, args: &HashMap<String, EmuValue>) -> Result<()> {
let process = &mut self.inner.borrow_mut().process;
process.update_status()?;
match process.state {
EmuState::On => {
bail!(EmuError::StartFailureCause(String::from(
"DUT is already running",
)));
}
EmuState::Busy => {
bail!(EmuError::StartFailureCause(String::from(
"DUT is in transient state BUSY",
)));
}
EmuState::Error => {
log::debug!("DUT trying to recover after error");
}
_ => {}
};
process.update_args(factory_reset, args)?;
process.spawn_process()?;
process.state = EmuState::On;
Ok(())
}
/// Stop emulator sub-process.
fn stop(&self) -> Result<()> {
let process = &mut self.inner.borrow_mut().process;
process.update_status()?;
match process.state {
EmuState::Off => {
bail!(EmuError::StopFailureCause(String::from(
"DUT is alredy Off"
),));
}
EmuState::Busy => {
bail!(EmuError::StopFailureCause(String::from(
"DUT is in transient state BUSY"
),));
}
EmuState::Error => {
log::info!("DUT stop after error");
}
_ => {}
}
process.stop_process()?;
Ok(())
}
}