blob: 63ffea80317c6c3c769d5f8b72293edce21ea3dc [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 anyhow::{anyhow, bail, Result};
use directories::{BaseDirs, ProjectDirs};
use erased_serde::Serialize;
use log::LevelFilter;
use nix::sys::signal::{self, Signal};
use nix::unistd::{dup2, setsid, Pid};
use std::env::{self, args_os, ArgsOs};
use std::ffi::OsString;
use std::fs::{self, read_to_string, File};
use std::io::{self, ErrorKind, Write};
use std::iter::{IntoIterator, Iterator};
use std::os::unix::io::AsRawFd;
use std::path::PathBuf;
use std::process::{self, ChildStdout, Command, Stdio};
use std::str::FromStr;
use std::time::Duration;
use structopt::StructOpt;
use opentitanlib::proxy::SessionHandler;
use opentitanlib::{backend, util};
#[derive(Debug, StructOpt)]
#[structopt(
name = "opentitansession",
about = "A tool for interacting with OpenTitan chips."
)]
struct Opts {
#[structopt(
long,
default_value = "config",
help = "Filename of a default flagsfile. Relative to $XDG_CONFIG_HOME/opentitantool."
)]
rcfile: PathBuf,
#[structopt(long, default_value = "off")]
logging: LevelFilter,
#[structopt(flatten)]
backend_opts: backend::BackendOpts,
#[structopt(
long,
help = "Stop a running session, optionally combine with --listen_port for disambiguation"
)]
stop: bool,
#[structopt(
long,
help = "Optional, defaults to 9900 or nearest higher available port."
)]
listen_port: Option<u16>,
#[structopt(
long,
help = "Start session, staying in foreground (do not daemonize). Session process will terminate if its parent dies."
)]
foreground: bool,
#[structopt(
long,
help = "Internal, used to tell the child process to run as a daemon."
)]
child: bool,
}
// Given some existing option configuration, maybe re-evaluate command
// line options by reading an `rcfile`.
fn parse_command_line(opts: Opts, mut args: ArgsOs) -> Result<Opts> {
// Initialize the logger if the user requested the non-defualt option.
let logging = opts.logging;
if logging != LevelFilter::Off {
env_logger::Builder::from_default_env()
.filter(None, opts.logging)
.init();
}
if opts.rcfile.as_os_str().is_empty() {
// No rcfile to parse.
return Ok(opts);
}
// Construct the rcfile path based on the user's config directory
// (ie: $HOME/.config/opentitantool/<filename>).
let rcfile = if let Some(base) = ProjectDirs::from("org", "opentitan", "opentitantool") {
base.config_dir().join(&opts.rcfile)
} else {
opts.rcfile
};
// argument[0] is the executable name.
let mut arguments = vec![args.next().unwrap()];
// Read in the rcfile and extend the argument list.
match read_to_string(&rcfile) {
Ok(content) => {
for line in content.split('\n') {
// Strip basic comments as shellwords won't handle comments.
let (line, _) = line.split_once('#').unwrap_or((line, ""));
arguments.extend(shellwords::split(line)?.iter().map(OsString::from));
}
Ok(())
}
Err(e) if e.kind() == ErrorKind::NotFound => {
log::warn!("Could not read {:?}. Ignoring.", rcfile);
Ok(())
}
Err(e) => Err(anyhow::Error::new(e).context(format!("Reading file {:?}", rcfile))),
}?;
// Extend the argument list with all remaining command line arguments.
arguments.extend(args.into_iter());
let opts = Opts::from_iter(&arguments);
if opts.logging != logging {
// Try re-initializing the logger. Ignore errors.
let _ = env_logger::Builder::from_default_env()
.filter(None, opts.logging)
.try_init();
}
Ok(opts)
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct SessionStartResult {
port: u16,
}
/// Spawn a child process, passing all the same arguments to the child, letting it instantiate a
/// Transport based on the command line arguments, listen on a TCP socket, and run as a daemon
/// process serving network requests. Success of the child is verified by means of a
/// `SessionStartResult` JSON message sent through the standard output pipe.
fn start_session(run_file_fn: impl FnOnce(u16) -> PathBuf) -> Result<Box<dyn Serialize>> {
let mut child = Command::new(env::current_exe()?) // Same executable
.arg("--child") // Add argument to let the new process know it is the daemon child
.args(args_os().skip(1)) // Propagate all existing arguments: --interface, etc.
.stdin(Stdio::null()) // Not used by child, disconnect from terminal
.stdout(Stdio::piped()) // Used for signalling completion of daemon startup
.stderr(Stdio::inherit()) // May be used for error messages during daemon startup
.spawn()?;
match serde_json::from_reader::<&mut ChildStdout, Result<SessionStartResult, String>>(
child.stdout.as_mut().unwrap(),
) {
Ok(Ok(result)) => {
// Create a pid file corresponding to the requested TCP port.
let path = run_file_fn(result.port);
File::create(path)?.write_all(format!("{}\n", child.id()).as_bytes())?;
Ok(Box::new(result))
}
Ok(Err(e)) => bail!(e),
Err(e) => bail!("Child process failed to start: {}", e),
}
}
// This method runs in the daemon child. It will instantiate SessionHandler to bind to a
// socket, then report the chosen port number to the parent process by means of a serialized
// `SessionStartResult` sent through the stdout anonymous pipe, and finally enter an infnite
// loop, processing connections on that socket
fn session_child(listen_port: Option<u16>, backend_opts: &backend::BackendOpts) -> Result<()> {
let transport = backend::create(backend_opts)?;
let mut session = SessionHandler::init(&transport, listen_port)?;
// Instantiation of Transport backend, and binding to a socket was successful, now go
// through the process of making this process a daemon, disconnected from the
// terminal that was used to start it.
// All configuration files have been processed (relative to current direction), we can now
// drop the reference to the file system, (in case the admin wants to unmount while this
// daemon is still running.)
env::set_current_dir("/")?;
// Close stderr, which remained open in order to allow any errors from the above code to
// surface, but needs to be severed in order for the daemon to avoid being killed by SIGHUP
// if the user closes the terminal window.
dup2(File::open("/dev/null")?.as_raw_fd(), 2)?;
// After severing the only connection to the controlling terminal inherited from the parent,
// we can now establish a new Unix "session" for this process, which will not be
// "controlled" by any terminal. This means that this daemon will not be killed by SIGHUP,
// in case the terminal that was used for running `session start` is later closed.
setsid()?;
// Report startup success to parent process.
serde_json::to_writer::<io::Stdout, Result<SessionStartResult, String>>(
io::stdout(),
&Ok(SessionStartResult {
port: session.get_port(),
}),
)?;
io::stdout().flush()?;
// Closing the standard output pipe is the signal to the parent process that this child has
// started up successfully. We close the pipe indirectly, by replacing file descriptor 1
// with one pointing to /dev/null. This will ensure that any subsequent accidentally
// executed println!() will be a no-op, rather than trigger termination via SIGPIPE.
dup2(2, 1)?;
// Indefinitely run command processing loop in this daemon process.
session.run_loop()
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct SessionStopResult {}
/// Load .pid file based on given port number, and send SIGTERM to the process identified in the
/// file, to request the daemon gracefully shut down.
fn stop_session(run_file_fn: impl FnOnce(u16) -> PathBuf, port: u16) -> Result<Box<dyn Serialize>> {
// Read the pid file corresponding to the requested TCP port.
let path = run_file_fn(port);
let pid: i32 = FromStr::from_str(fs::read_to_string(&path)?.trim())?;
// Send signal to daemon process, asking it to terminate.
signal::kill(Pid::from_raw(pid), Signal::SIGTERM)?;
// Wait for daemon process to stop.
loop {
std::thread::sleep(Duration::from_millis(100));
// Send "signal 0", meaning that the kernel performs error checks (among those, checking
// that the target process exists), without actually sending any signal.
match signal::kill(Pid::from_raw(pid), None) {
Ok(()) => (), // Process still running, repeat.
Err(nix::Error::Sys(nix::errno::Errno::ESRCH)) => {
// Process could not be found, meaning that it has terminated, as expected.
fs::remove_file(&path)?;
return Ok(Box::new(SessionStopResult {}));
}
Err(e) => bail!("Unexpected error querying process presence: {}", e),
}
}
}
fn main() -> Result<()> {
let opts = parse_command_line(Opts::from_args(), args_os())?;
if opts.foreground {
// Start session process in foreground (do not daemonize). The session process will
// terminate if its parent dies. This might be useful for use in scripts.
// Request a SIGTERM if our parent dies.
util::nix::request_parent_death_signal(Signal::SIGTERM)?;
let transport = backend::create(&opts.backend_opts)?;
let mut session = SessionHandler::init(&transport, opts.listen_port)?;
println!("Listening on port {}", session.get_port());
session.run_loop()?;
return Ok(());
}
if opts.child {
// This process is a child, which is supposed to stay running as a daemon.
match session_child(opts.listen_port, &opts.backend_opts) {
Ok(()) => process::exit(0),
Err(e) => {
// Report any error to parent process though stdout pipe.
serde_json::to_writer::<io::Stdout, Result<SessionStartResult, String>>(
io::stdout(),
&Err(format!("{}", e)),
)?;
process::exit(1)
}
}
}
// Locate directory to use for .pid files
let base_dirs = BaseDirs::new().unwrap();
let run_user_dir = base_dirs
.runtime_dir()
.ok_or_else(|| anyhow!("No /run/user directory"))?;
let run_file_fn = |port: u16| {
let mut p = PathBuf::from(run_user_dir);
p.push(format!("opentitansession.{}.pid", port));
p
};
let value = if opts.stop {
// Send signal to daemon process to stop
stop_session(run_file_fn, opts.listen_port.unwrap_or(9900))?
} else {
// Fork a daemon process
start_session(run_file_fn)?
};
println!("{}", serde_json::to_string_pretty(&value)?);
Ok(())
}