various low level system utils moved into system module

This commit is contained in:
Bruce Leidl 2019-04-02 15:00:01 -04:00
parent 4bd8c3626f
commit 4b4e5f31e7
7 changed files with 435 additions and 166 deletions

View File

@ -1,64 +0,0 @@
use std::path::{PathBuf,Path};
use std::fs;
use crate::Result;
pub struct Mount {
source: String,
target: PathBuf,
fstype: String,
options: String,
}
impl Mount {
///
/// Returns `true` if `path` matches the source field (first field)
/// of any of the mount lines listed in /proc/mounts
///
pub fn is_source_mounted<P: AsRef<Path>>(path: P) -> Result<bool> {
let path_str = path.as_ref().to_string_lossy();
let mounts = Mount::all_mounts()?;
Ok(mounts.into_iter().any(|m| m.source == path_str))
}
pub fn is_target_mounted<P: AsRef<Path>>(path: P) -> Result<bool> {
let mounts = Mount::all_mounts()?;
Ok(mounts.into_iter().any(|m| m.target == path.as_ref()))
}
pub fn all_mounts() -> Result<Vec<Mount>> {
let s = fs::read_to_string("/proc/mounts")?;
Ok(s.lines().flat_map(Mount::parse_mount_line).collect())
}
fn parse_mount_line(line: &str) -> Option<Mount> {
let parts = line.split_whitespace().collect::<Vec<_>>();
if parts.len() < 4 {
warn!("Failed to parse mount line: {}", line);
return None;
}
Some(Mount{
source: parts[0].to_string(),
target: PathBuf::from(parts[1]),
fstype: parts[2].to_string(),
options: parts[3].to_string(),
})
}
pub fn source(&self) -> &str {
&self.source
}
pub fn target(&self) -> &Path {
&self.target
}
pub fn fstype(&self) -> &str {
&self.fstype
}
pub fn options(&self) -> &str {
&self.options
}
}

View File

@ -0,0 +1,78 @@
use std::fs::{self,File,OpenOptions};
use std::io::{Error,ErrorKind};
use std::os::unix::io::AsRawFd;
use std::path::{Path,PathBuf};
use crate::Result;
pub struct FileLock {
file: File,
path: PathBuf,
}
impl FileLock {
pub fn acquire<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref().to_path_buf();
let file = Self::open_lockfile(&path)?;
let flock = FileLock { file, path };
flock.lock()?;
Ok(flock)
}
fn open_lockfile(path: &Path) -> Result<File> {
if let Some(parent) = path.parent() {
if !parent.exists() {
fs::create_dir_all(parent)?;
}
}
for _ in 0..3 {
if let Some(file) = Self::try_create_lockfile(path)? {
return Ok(file);
}
if let Some(file) = Self::try_open_lockfile(path)? {
return Ok(file);
}
}
Err(format_err!("unable to acquire lockfile {}", path.display() ))
}
fn try_create_lockfile(path: &Path) -> Result<Option<File>> {
match OpenOptions::new().write(true).create_new(true).open(path) {
Ok(file) => Ok(Some(file)),
Err(ref e) if e.kind() == ErrorKind::AlreadyExists => Ok(None),
Err(e) => Err(e.into()),
}
}
fn try_open_lockfile(path: &Path) -> Result<Option<File>> {
match File::open(path) {
Ok(file) => Ok(Some(file)),
Err(ref e) if e.kind() == ErrorKind::NotFound => Ok(None),
Err(e) => Err(e.into()),
}
}
fn unlock(&self) -> Result<()> {
self.flock(libc::LOCK_UN)
}
fn lock(&self) -> Result<()> {
self.flock(libc::LOCK_EX)
}
fn flock(&self, flag: libc::c_int) -> Result<()> {
if unsafe { libc::flock(self.file.as_raw_fd(), flag) } < 0 {
return Err(Error::last_os_error().into());
}
Ok(())
}
}
impl Drop for FileLock {
fn drop(&mut self) {
let _ = fs::remove_file(&self.path);
let _ = self.unlock();
}
}

View File

@ -0,0 +1,120 @@
use std::fmt;
use std::path::{Path,PathBuf};
use crate::Result;
use super::mounts::Mounts;
#[derive(Debug)]
pub struct LoopDevice(PathBuf);
impl LoopDevice {
const LOSETUP: &'static str = "/usr/sbin/losetup";
const MOUNT: &'static str = "/usr/bin/mount";
fn new<P: AsRef<Path>>(device: P) -> LoopDevice {
let device = device.as_ref().to_path_buf();
LoopDevice(device)
}
pub fn create<P: AsRef<Path>>(image: P, offset: Option<usize>, read_only: bool) -> Result<LoopDevice> {
let image = image.as_ref();
let mut args = String::new();
if let Some(offset) = offset {
args += &format!("--offset {} ", offset);
}
if read_only {
args += &format!("--read-only ");
}
args += &format!("-f --show {}", image.display());
let output = cmd_with_output!(Self::LOSETUP, args)?;
Ok(LoopDevice::new(output))
}
pub fn with_loop<P,F,R>(image: P, offset: Option<usize>, read_only: bool, f: F) -> Result<R>
where P: AsRef<Path>,
F: FnOnce(&LoopDevice) -> Result<R>,
{
let loopdev = Self::create(image, offset, read_only)?;
let result = f(&loopdev);
let detach_result = loopdev.detach();
let r = result?;
detach_result.map_err(|e| format_err!("error detaching loop device: {}", e))?;
Ok(r)
}
/// Search for an entry in /proc/mounts for a loop device which is mounted on the
/// specified mountpoint.
/// The relevant lines look like this:
///
/// /dev/loop3 /run/citadel/realmfs/realmfs-name-rw.mountpoint ext4 rw,noatime,data=ordered 0 0
///
pub fn find_mounted_loop<P: AsRef<Path>>(mount_target: P) -> Option<LoopDevice> {
let mount_target = mount_target.as_ref();
Mounts::load().ok()
.and_then(|mounts| mounts.mounts()
.find(|m| m.target_path() == mount_target &&
m.source().starts_with("/dev/loop"))
.map(|m| LoopDevice::new(m.source_path())) )
}
pub fn find_devices_for<P: AsRef<Path>>(image: P) -> Result<Vec<LoopDevice>> {
let image = image.as_ref();
// Output from losetup -j looks like this:
// /dev/loop1: [0036]:64845938 (/storage/resources/dev/citadel-extra-dev-001.img), offset 4096
let output:String = cmd_with_output!(Self::LOSETUP, "-j {}", image.display())?;
Ok(output.lines()
.flat_map(|line| line.splitn(2, ":").next())
.map(|s| LoopDevice::new(s))
.collect())
}
pub fn detach(&self) -> Result<()> {
cmd!(Self::LOSETUP, format!("-d {}", self.0.display()))
}
pub fn resize(&self) -> Result<()> {
cmd!(Self::LOSETUP, format!("-c {}", self.0.display()))
}
pub fn device(&self) -> &Path {
&self.0
}
pub fn device_str(&self) -> &str {
self.device().to_str().unwrap()
}
pub fn mount_ro<P: AsRef<Path>>(&self, target: P) -> Result<()> {
let target = target.as_ref();
cmd!(Self::MOUNT, "-oro,noatime {} {}", self, target.display())
}
pub fn mount<P: AsRef<Path>>(&self, target: P) -> Result<()> {
let target = target.as_ref();
cmd!(Self::MOUNT, "-orw,noatime {} {}", self, target.display())
}
pub fn mount_pair<P,Q>(&self, rw_target: P, ro_target: Q) -> Result<()>
where P: AsRef<Path>,
Q: AsRef<Path>
{
let rw = rw_target.as_ref();
let ro = ro_target.as_ref();
self.mount(rw)?;
// From mount(8):
//
// mount --bind olddir newdir
// mount -o remount,bind,ro olddir newdir
cmd!(Self::MOUNT, "--bind {} {}", rw.display(), ro.display())?;
cmd!(Self::MOUNT, "-o remount,bind,ro {} {}", rw.display(), ro.display())?;
Ok(())
}
}
impl fmt::Display for LoopDevice {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.device().display())
}
}

View File

@ -0,0 +1,9 @@
mod lock;
mod loopdev;
mod mounts;
mod uname;
pub use self::uname::UtsName;
pub use self::loopdev::LoopDevice;
pub use self::mounts::{Mounts,MountLine};
pub use self::lock::FileLock;

View File

@ -0,0 +1,96 @@
use std::fs;
use std::collections::HashMap;
use std::path::Path;
use crate::Result;
pub struct Mounts {
content: String,
}
impl Mounts {
///
/// Returns `true` if `path` matches the source field (first field)
/// of any of the mount lines listed in /proc/mounts
///
pub fn is_source_mounted<P: AsRef<Path>>(path: P) -> Result<bool> {
let path = path.as_ref();
let mounted = Self::load()?
.mounts()
.any(|m| m.source_path() == path);
Ok(mounted)
}
pub fn is_target_mounted<P: AsRef<Path>>(path: P) -> Result<bool> {
let path = path.as_ref();
let mounted = Self::load()?
.mounts()
.any(|m| m.target_path() == path);
Ok(mounted)
}
pub fn load() -> Result<Mounts> {
let content = fs::read_to_string("/proc/mounts")?;
Ok(Mounts { content })
}
pub fn mounts(&self) -> impl Iterator<Item=MountLine> {
self.content.lines().flat_map(MountLine::new)
}
}
pub struct MountLine<'a> {
line: &'a str,
}
impl <'a> MountLine<'a> {
fn new(line: &str) -> Option<MountLine> {
if line.split_whitespace().count() >= 4 {
Some(MountLine { line })
} else {
None
}
}
fn field(&self, n: usize) -> &str {
self.line.split_whitespace().nth(n).unwrap()
}
pub fn source(&self) -> &str {
self.field(0)
}
pub fn source_path(&self) -> &Path {
Path::new(self.source())
}
pub fn target(&self) -> &str {
self.field(1)
}
pub fn target_path(&self) -> &Path {
Path::new(self.target())
}
pub fn fstype(&self) -> &str {
self.field(2)
}
pub fn options(&self) -> HashMap<&str,&str> {
self.field(3).split(',').map(Self::parse_key_val).collect()
}
fn parse_key_val(option: &str) -> (&str,&str) {
let kv: Vec<&str> = option.splitn(2, '=').collect();
if kv.len() == 2 {
(kv[0], kv[1])
} else {
(kv[0], "")
}
}
}

View File

@ -0,0 +1,48 @@
use std::ffi::CStr;
use std::mem;
use std::str;
use libc::c_char;
#[repr(C)]
#[derive(Clone, Copy)]
pub struct UtsName(libc::utsname);
#[allow(dead_code)]
impl UtsName {
pub fn uname() -> UtsName {
unsafe {
let mut ret: UtsName = mem::uninitialized();
libc::uname(&mut ret.0);
ret
}
}
pub fn sysname(&self) -> &str {
to_str(&(&self.0.sysname as *const c_char ) as *const *const c_char)
}
pub fn nodename(&self) -> &str {
to_str(&(&self.0.nodename as *const c_char ) as *const *const c_char)
}
pub fn release(&self) -> &str {
to_str(&(&self.0.release as *const c_char ) as *const *const c_char)
}
pub fn version(&self) -> &str {
to_str(&(&self.0.version as *const c_char ) as *const *const c_char)
}
pub fn machine(&self) -> &str {
to_str(&(&self.0.machine as *const c_char ) as *const *const c_char)
}
}
#[inline]
fn to_str<'a>(s: *const *const c_char) -> &'a str {
unsafe {
let res = CStr::from_ptr(*s).to_bytes();
str::from_utf8_unchecked(res)
}
}

View File

@ -1,14 +1,15 @@
use std::path::{Path,PathBuf}; use std::path::{Path,PathBuf};
use std::process::{Command,ExitStatus,Stdio}; use std::process::{Command,Stdio};
use std::mem; use std::os::unix::ffi::OsStrExt;
use libc::{self, c_char}; use std::os::unix::fs::MetadataExt;
use std::ffi::CStr;
use std::str::from_utf8_unchecked;
use std::env; use std::env;
use std::fs::File; use std::fs::{self,File};
use std::ffi::CString;
use std::io::{self, Seek, Read, BufReader, SeekFrom}; use std::io::{self, Seek, Read, BufReader, SeekFrom};
use failure::ResultExt; use failure::ResultExt;
use walkdir::WalkDir;
use libc;
use crate::Result; use crate::Result;
@ -56,55 +57,11 @@ pub fn ensure_command_exists(cmd: &str) -> Result<()> {
Err(format_err!("Cannot execute '{}': command does not exist", cmd)) Err(format_err!("Cannot execute '{}': command does not exist", cmd))
} }
pub fn exec_cmdline<S: AsRef<str>>(cmd_path: &str, args: S) -> Result<()> {
ensure_command_exists(cmd_path)?;
let args: Vec<&str> = args.as_ref().split_whitespace().collect::<Vec<_>>();
let status = Command::new(cmd_path)
.args(args)
.stderr(Stdio::inherit())
.status()?;
check_cmd_status(cmd_path, &status)
}
pub fn exec_cmdline_quiet<S: AsRef<str>>(cmd_path: &str, args: S) -> Result<()> {
ensure_command_exists(cmd_path)?;
let args: Vec<&str> = args.as_ref().split_whitespace().collect::<Vec<_>>();
let status = Command::new(cmd_path)
.args(args)
.stderr(Stdio::null())
.stdout(Stdio::null())
.status()?;
check_cmd_status(cmd_path, &status)
}
pub fn exec_cmdline_with_output<S: AsRef<str>>(cmd_path: &str, args: S) -> Result<String> {
ensure_command_exists(cmd_path)?;
let args: Vec<&str> = args.as_ref().split_whitespace().collect::<Vec<_>>();
let res = Command::new(cmd_path)
.args(args)
.stderr(Stdio::inherit())
.output()
.context(format!("unable to execute {}", cmd_path))?;
check_cmd_status(cmd_path, &res.status)?;
Ok(String::from_utf8(res.stdout).unwrap().trim().to_owned())
}
fn check_cmd_status(cmd_path: &str, status: &ExitStatus) -> Result<()> {
if !status.success() {
match status.code() {
Some(code) => bail!("command {} failed with exit code: {}", cmd_path, code),
None => bail!("command {} failed with no exit code", cmd_path),
}
}
Ok(())
}
pub fn sha256<P: AsRef<Path>>(path: P) -> Result<String> { pub fn sha256<P: AsRef<Path>>(path: P) -> Result<String> {
let output = exec_cmdline_with_output("/usr/bin/sha256sum", format!("{}", path.as_ref().display())) let path = path.as_ref();
.context(format!("failed to calculate sha256 on {}", path.as_ref().display()))?; let output = cmd_with_output!("/usr/bin/256sum", "{}", path.display())
.context(format!("failed to calculate sha256 on {}", path.display()))?;
let v: Vec<&str> = output.split_whitespace().collect(); let v: Vec<&str> = output.split_whitespace().collect();
Ok(v[0].trim().to_owned()) Ok(v[0].trim().to_owned())
@ -158,71 +115,96 @@ pub fn exec_cmdline_pipe_input<S,P>(cmd_path: &str, args: S, input: P, range: Fi
} }
pub fn xz_compress<P: AsRef<Path>>(path: P) -> Result<()> { pub fn xz_compress<P: AsRef<Path>>(path: P) -> Result<()> {
exec_cmdline("/usr/bin/xz", format!("-T0 {}", path.as_ref().display())) let path = path.as_ref();
.context(format!("failed to compress {}", path.as_ref().display()))?; cmd!("/usr/bin/xz", "-T0 {}", path.display())
.context(format!("failed to compress {}", path.display()))?;
Ok(()) Ok(())
} }
pub fn xz_decompress<P: AsRef<Path>>(path: P) -> Result<()> { pub fn xz_decompress<P: AsRef<Path>>(path: P) -> Result<()> {
exec_cmdline("/usr/bin/xz", format!("-d {}", path.as_ref().display())) let path = path.as_ref();
.context(format!("failed to decompress {}", path.as_ref().display()))?; cmd!("/usr/bin/xz", "-d {}", path.display())
.context(format!("failed to decompress {}", path.display()))?;
Ok(()) Ok(())
} }
pub fn mount<P: AsRef<Path>>(source: &str, target: P, options: Option<&str>) -> Result<()> { pub fn mount<P: AsRef<Path>>(source: impl AsRef<str>, target: P, options: Option<&str>) -> Result<()> {
let paths = format!("{} {}", source, target.as_ref().display()); let source = source.as_ref();
let args = match options { let target = target.as_ref();
Some(s) => format!("{} {}", s, paths), if let Some(options) = options {
None => paths, cmd!("/usr/bin/mount", "{} {} {}", options, source, target.display())
}; } else {
exec_cmdline("/usr/bin/mount", args) cmd!("/usr/bin/mount", "{} {}", source, target.display())
}
} }
pub fn umount<P: AsRef<Path>>(path: P) -> Result<()> { pub fn umount<P: AsRef<Path>>(path: P) -> Result<()> {
let args = format!("{}", path.as_ref().display()); let path = path.as_ref();
exec_cmdline("/usr/bin/umount", args) cmd!("/usr/bin/umount", "{}", path.display())
} }
pub fn chown_user<P: AsRef<Path>>(path: P) -> io::Result<()> {
#[repr(C)] chown(path.as_ref(), 1000, 1000)
#[derive(Clone, Copy)]
pub struct UtsName(libc::utsname);
#[allow(dead_code)]
impl UtsName {
pub fn sysname(&self) -> &str {
to_str(&(&self.0.sysname as *const c_char ) as *const *const c_char)
} }
pub fn nodename(&self) -> &str { pub fn chown(path: &Path, uid: u32, gid: u32) -> io::Result<()> {
to_str(&(&self.0.nodename as *const c_char ) as *const *const c_char) let cstr = CString::new(path.as_os_str().as_bytes())?;
}
pub fn release(&self) -> &str {
to_str(&(&self.0.release as *const c_char ) as *const *const c_char)
}
pub fn version(&self) -> &str {
to_str(&(&self.0.version as *const c_char ) as *const *const c_char)
}
pub fn machine(&self) -> &str {
to_str(&(&self.0.machine as *const c_char ) as *const *const c_char)
}
}
pub fn uname() -> UtsName {
unsafe { unsafe {
let mut ret: UtsName = mem::uninitialized(); if libc::chown(cstr.as_ptr(), uid, gid) == -1 {
libc::uname(&mut ret.0); return Err(io::Error::last_os_error());
ret
} }
} }
Ok(())
}
#[inline] fn copy_path(from: &Path, to: &Path, chown_to: Option<(u32,u32)>) -> Result<()> {
fn to_str<'a>(s: *const *const c_char) -> &'a str { if to.exists() {
unsafe { bail!("destination path {} already exists which is not expected", to.display());
let res = CStr::from_ptr(*s).to_bytes(); }
from_utf8_unchecked(res)
let meta = from.metadata()?;
if from.is_dir() {
fs::create_dir(to)?;
} else {
fs::copy(&from, &to)?;
}
if let Some((uid,gid)) = chown_to {
chown(to, uid, gid)?;
} else {
chown(to, meta.uid(), meta.gid())?;
}
Ok(())
}
pub fn copy_tree(from_base: &Path, to_base: &Path) -> Result<()> {
_copy_tree(from_base, to_base, None)
}
pub fn copy_tree_with_chown(from_base: &Path, to_base: &Path, chown_to: (u32,u32)) -> Result<()> {
_copy_tree(from_base, to_base, Some(chown_to))
}
fn _copy_tree(from_base: &Path, to_base: &Path, chown_to: Option<(u32,u32)>) -> Result<()> {
for entry in WalkDir::new(from_base) {
let path = entry?.path().to_owned();
let to = to_base.join(path.strip_prefix(from_base)?);
if &to != to_base {
copy_path(&path, &to, chown_to)
.map_err(|e| format_err!("failed to copy {} to {}: {}", path.display(), to.display(), e))?;
} }
} }
Ok(())
}
pub fn chown_tree(base: &Path, chown_to: (u32,u32), include_base: bool) -> Result<()> {
for entry in WalkDir::new(base) {
let entry = entry?;
if entry.path() != base || include_base {
chown(entry.path(), chown_to.0, chown_to.1)?;
}
}
Ok(())
}