use crate::app_entry::*;
use crate::error::*;
use fs_extra;
use kubos_app::RunLevel;
use log::*;
use std::fs;
use std::io::Read;
use std::os::unix;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
use toml;
pub const K_APPS_DIR: &str = "/home/system/kubos/apps";
#[derive(Clone, Debug)]
pub struct AppRegistry {
#[doc(hidden)]
pub entries: Arc<Mutex<Vec<AppRegistryEntry>>>,
pub apps_dir: String,
}
impl AppRegistry {
pub fn new_from_dir(apps_dir: &str) -> Result<AppRegistry, AppError> {
let registry = AppRegistry {
entries: Arc::new(Mutex::new(Vec::new())),
apps_dir: String::from(apps_dir),
};
let active_dir = PathBuf::from(format!("{}/active", apps_dir));
if !active_dir.exists() {
fs::create_dir_all(&active_dir)?;
}
registry
.entries
.lock()
.map_err(|err| AppError::RegistryError {
err: format!("Couldn't get entries mutex: {:?}", err),
})?
.extend(registry.discover_apps()?);
Ok(registry)
}
pub fn new() -> Result<AppRegistry, AppError> {
Self::new_from_dir(K_APPS_DIR)
}
fn discover_apps(&self) -> Result<Vec<AppRegistryEntry>, AppError> {
let mut reg_entries: Vec<AppRegistryEntry> = Vec::new();
for entry in fs::read_dir(&self.apps_dir)? {
if let Ok(entry) = entry {
if let Ok(file_type) = entry.file_type() {
if file_type.is_dir() && entry.file_name().to_str() != Some("active") {
reg_entries.extend(self.discover_versions(entry.path())?);
}
}
}
}
Ok(reg_entries)
}
fn discover_versions(&self, app_dir: PathBuf) -> Result<Vec<AppRegistryEntry>, AppError> {
let mut reg_entries: Vec<AppRegistryEntry> = Vec::new();
for version in fs::read_dir(app_dir)? {
if version.is_err() {
continue;
}
let version = version.unwrap();
match version
.file_type()
.and_then(|file_type| Ok(file_type.is_dir()))
{
Ok(true) => {
if let Ok(entry) = AppRegistryEntry::from_dir(&version.path()) {
if entry.active_version {
self.set_active(&entry.app.name, &version.path().to_string_lossy())?;
}
reg_entries.push(entry);
} else {
let _ = fs::remove_dir_all(version.path());
}
}
_ => {
let _ = fs::remove_dir_all(version.path());
}
}
}
Ok(reg_entries)
}
fn set_active(&self, name: &str, app_dir: &str) -> Result<(), AppError> {
let active_symlink = PathBuf::from(format!("{}/active/{}", self.apps_dir, name));
if active_symlink.exists() {
if let Err(err) = fs::remove_file(active_symlink.clone()) {
return Err(AppError::RegisterError {
err: format!(
"Couldn't remove symlink {}: {:?}",
active_symlink.display(),
err
),
});
}
}
if let Err(err) = unix::fs::symlink(app_dir, active_symlink.clone()) {
return Err(AppError::RegisterError {
err: format!(
"Couldn't symlink {} to {}: {:?}",
active_symlink.display(),
app_dir,
err
),
});
}
Ok(())
}
pub fn register(&self, path: &str) -> Result<AppRegistryEntry, AppError> {
let app_path = Path::new(path);
if !app_path.exists() {
return Err(AppError::RegisterError {
err: format!("{} does not exist", path),
});
}
if !app_path.is_dir() {
return Err(AppError::RegisterError {
err: format!("{} is not a directory", path),
});
}
let mut data = String::new();
fs::File::open(app_path.join("manifest.toml"))
.and_then(|mut fp| fp.read_to_string(&mut data))?;
let metadata: AppMetadata = match toml::from_str(&data) {
Ok(val) => val,
Err(error) => {
return Err(AppError::ParseError {
entity: "manifest.toml".to_owned(),
err: error.to_string(),
});
}
};
let app_name = metadata.name.clone();
let app_exec = if let Some(path) = metadata.executable.clone() {
path
} else {
metadata.name.clone()
};
if !app_path.join(app_exec.clone()).exists() {
return Err(AppError::RegisterError {
err: format!("Application file {} not found in given path", app_name),
});
}
let mut entries = self.entries.lock().map_err(|err| AppError::RegisterError {
err: format!("Couldn't get entries mutex: {:?}", err),
})?;
let old_active = if Path::new(&format!("{}/{}", self.apps_dir, app_name)).exists() {
entries
.iter()
.position(|ref e| e.active_version && e.app.name == app_name)
} else {
None
};
let app_dir_str = format!("{}/{}/{}", self.apps_dir, app_name, metadata.version);
let app_dir = Path::new(&app_dir_str);
if app_dir.exists() {
return Err(AppError::RegisterError {
err: format!(
"App {} version {} already exists",
app_name, metadata.version
),
});
} else {
fs::create_dir_all(app_dir)?;
}
let files: Vec<PathBuf> = fs::read_dir(app_path)?
.filter_map(|file| {
if let Ok(entry) = file {
Some(entry.path())
} else {
None
}
})
.collect();
fs_extra::copy_items(&files, app_dir, &fs_extra::dir::CopyOptions::new()).map_err(
|error| {
let _ = fs::remove_dir_all(app_dir);
let _ = fs::remove_dir(format!("{}/{}", self.apps_dir, app_name));
AppError::RegisterError {
err: format!("Error copying files into registry dir: {}", error),
}
},
)?;
let reg_entry = AppRegistryEntry {
app: App {
name: app_name.clone(),
executable: format!("{}/{}", app_dir_str, app_exec),
version: metadata.version,
author: metadata.author,
},
active_version: true,
};
entries.push(reg_entry);
entries[entries.len() - 1].save().or_else(|err| {
let _ = fs::remove_dir_all(app_dir);
let _ = fs::remove_dir(format!("{}/{}", self.apps_dir, app_name));
Err(err)
})?;
if let Some(index) = old_active {
entries[index].active_version = false;
entries[index].save()?;
}
self.set_active(&app_name, &app_dir_str)?;
Ok(entries[entries.len() - 1].clone())
}
pub fn uninstall(&self, app_name: &str, version: &str) -> Result<bool, AppError> {
let mut errors = None;
let app_dir = format!("{}/{}/{}", self.apps_dir, app_name, version);
if let Err(error) = fs::remove_dir_all(app_dir) {
errors = Some(format!("Failed to remove app directory: {}", error));
}
let _ = fs::remove_dir(format!("{}/{}", self.apps_dir, app_name));
let mut entries = self
.entries
.lock()
.map_err(|err| AppError::UninstallError {
err: format!("Couldn't get entries mutex: {:?}", err),
})?;
match entries
.iter()
.position(|ref e| e.app.name == app_name && e.app.version == version)
{
Some(index) => {
entries.remove(index);
}
None => {
if let Some(error) = errors {
errors = Some(format!(
"{}. {} version {} not found in registry",
error, app_name, version
));
} else {
errors = Some(format!(
"{} version {} not found in registry",
app_name, version
));
}
}
}
match errors {
Some(err) => Err(AppError::UninstallError { err }),
None => Ok(true),
}
}
pub fn uninstall_all(&self, app_name: &str) -> Result<bool, AppError> {
let mut errors = vec![];
let app_dir = format!("{}/{}", self.apps_dir, app_name);
if let Err(error) = fs::remove_dir_all(app_dir) {
errors.push(format!("Failed to remove app directory: {}", error));
}
if let Err(error) = fs::remove_file(format!("{}/active/{}", self.apps_dir, app_name)) {
errors.push(format!(
"Failed to remove active symlink for {}: {}",
app_name, error
));
}
match self.entries.lock() {
Ok(mut entries) => {
entries.retain(|entry| entry.app.name != app_name);
}
Err(err) => errors.push(format!("Couldn't get entries mutex: {:?}", err)),
}
if errors.is_empty() {
Ok(true)
} else {
let err = errors.join(". ");
Err(AppError::UninstallError { err })
}
}
pub fn set_version(&self, app_name: &str, version: &str) -> Result<(), AppError> {
let mut entries = self.entries.lock().map_err(|err| AppError::RegistryError {
err: format!("Couldn't get entries mutex: {:?}", err),
})?;
let curr_active = entries
.iter()
.position(|ref e| e.active_version && e.app.name == app_name);
if let Some(index) = curr_active {
if entries[index].app.version == version {
return Ok(());
}
}
let new_active = entries
.iter()
.position(|ref e| e.app.name == app_name && e.app.version == version)
.ok_or(AppError::RegistryError {
err: format!("App {} version {} not found in registry", app_name, version),
})?;
entries[new_active].active_version = true;
entries[new_active]
.save()
.map_err(|error| AppError::RegistryError {
err: format!("Failed to update new active version entry: {:?}", error),
})?;
if let Some(index) = curr_active {
entries[index].active_version = false;
entries[index]
.save()
.map_err(|error| AppError::RegistryError {
err: format!("Failed to update old active version entry: {:?}", error),
})?;
}
self.set_active(
&app_name,
&format!("{}/{}/{}", self.apps_dir, app_name, version),
)?;
Ok(())
}
pub fn start_app(
&self,
app_name: &str,
run_level: &RunLevel,
args: Option<Vec<String>>,
) -> Result<u32, AppError> {
let app = {
let entries = self.entries.lock().map_err(|err| AppError::StartError {
err: format!("Couldn't get entries mutex: {:?}", err),
})?;
match entries
.iter()
.find(|ref e| e.active_version && e.app.name == app_name)
{
Some(entry) => entry.app.clone(),
None => {
return Err(AppError::StartError {
err: format!("No active version found for app {}", app_name),
});
}
}
};
let app_path = PathBuf::from(&app.executable);
if !app_path.exists() {
let msg = match self.uninstall(&app.name, &app.version) {
Ok(_) => format!(
"{} does not exist. {} version {} automatically uninstalled",
app.executable, app.name, app.version
),
Err(error) => format!("{} does not exist. {}", app.executable, error),
};
return Err(AppError::StartError { err: msg });
}
let mut cmd = Command::new(app_path);
cmd.arg("-r").arg(format!("{}", run_level));
if let Some(add_args) = args {
cmd.args(&add_args);
}
let mut child = cmd.spawn().map_err(|err| AppError::StartError {
err: format!("Failed to spawn app: {:?}", err),
})?;
thread::sleep(Duration::from_millis(100));
if let Some(status) = child.try_wait().unwrap_or(None) {
if !status.success() {
Err(AppError::StartError {
err: format!("App returned {}", status),
})
} else {
Ok(child.id())
}
} else {
Ok(child.id())
}
}
pub fn run_onboot(&self) -> Result<(), AppError> {
let mut apps_started = 0;
let mut apps_not_started = 0;
let active_symlink = PathBuf::from(format!("{}/active", self.apps_dir));
if !active_symlink.exists() {
return Err(AppError::FileError {
err: "Failed to get list of active applications".to_owned(),
});
}
for entry in fs::read_dir(active_symlink)? {
match entry {
Ok(file) => {
let name = file.file_name();
match self.start_app(&name.to_string_lossy(), &RunLevel::OnBoot, None) {
Ok(_) => apps_started += 1,
Err(error) => {
error!("Failed to start {}: {}", name.to_string_lossy(), error);
apps_not_started += 1
}
}
}
Err(_) => apps_not_started += 1,
}
}
info!(
"Apps started: {}, Apps failed: {}",
apps_started, apps_not_started
);
if apps_not_started != 0 {
return Err(AppError::SystemError {
err: format!("Failed to start {} app/s", apps_not_started),
});
}
Ok(())
}
}