citadel/citadel-tools/citadel-appimg/src/manager.rs

298 lines
8.5 KiB
Rust

use std::path::{Path,PathBuf};
use std::io::Write;
use std::fs;
use std::os::unix::fs::{OpenOptionsExt,symlink};
use std::collections::HashMap;
use AppImg;
use Result;
use util::*;
use systemd;
lazy_static!{
static ref APPIMG_BASE_PATH: PathBuf = PathBuf::from("/storage/appimg");
static ref APPIMG_RUN_PATH: PathBuf = PathBuf::from("/run/appimg");
}
const DESKTOPD_SERVICE: &str = "citadel-desktopd.service";
pub struct ImageManager {
images: HashMap<String, AppImg>,
default: Option<String>,
current: Option<String>,
}
impl ImageManager {
fn new() -> Result<ImageManager> {
let default = default_appimg()?;
let current = current_appimg()?;
Ok(ImageManager {
images: HashMap::new(),
default, current,
})
}
pub fn load() -> Result<ImageManager> {
let mut manager = ImageManager::new()?;
for dent in fs::read_dir(APPIMG_BASE_PATH.as_path())? {
let path = dent?.path();
manager.process_path(&path)?;
}
Ok(manager)
}
fn process_path(&mut self, path: &Path) -> Result<()> {
let meta = path.symlink_metadata()?;
if !meta.is_dir() {
return Ok(())
}
let name = path_filename(path);
if !is_valid_name(name) {
warn!("ignoring directory in appimg storage which has invalid appimg name: {}", name);
return Ok(())
}
let appimg = AppImg::new(name)?;
self.images.insert(appimg.name().to_string(), appimg);
Ok(())
}
pub fn list(&self) -> Result<()> {
for img in self.images.values() {
let cur = if self.is_current(img) { "(current)" } else { "" };
let def = if self.is_default(img) { "(default)" } else { "" };
let run = if img.is_running() { "[running]"} else {""};
println!(" {} {} {} {}", img.name(), run, def, cur);
}
Ok(())
}
pub fn start_default(&mut self) -> Result<()> {
let name = match self.default {
Some(ref s) => s.clone(),
None => bail!("No default image to start"),
};
self.start_image(&name)
}
pub fn start_image(&mut self, name: &str) -> Result<()> {
// if current is not set, set it to this instance
let set_as_current = self.current.is_none();
if set_as_current {
self.set_current_target(name)?;
}
match self.images.get(name) {
Some(img) => {
img.start()?;
},
None => {
// XXX if set_as_current do something here to undo what set_current has done
warn!("Cannot start '{}'. Image does not exist");
return Ok(())
},
}
if set_as_current {
systemd::systemctl_restart(DESKTOPD_SERVICE);
}
Ok(())
}
pub fn stop_image(&mut self, name: &str) -> Result<()> {
match self.images.get(name) {
Some(img) => img.stop(),
None => {
warn!("Cannot stop '{}'. Image does not exist");
return Ok(())
},
}
let current = match self.current {
Some(ref s) => s.clone(),
None => return Ok(()),
};
if current == name {
systemd::systemctl_stop(DESKTOPD_SERVICE);
let path = APPIMG_RUN_PATH.join("current.appimg");
if path.exists() {
fs::remove_file(&path)?;
}
if let Some(img_name) = self.find_running_image_name() {
self.set_current_target(&img_name)?;
systemd::systemctl_start(DESKTOPD_SERVICE);
}
}
Ok(())
}
fn find_running_image_name(&self) -> Option<String> {
for img in self.images.values() {
if img.is_running() {
return Some(img.name().to_string());
}
}
None
}
fn is_current(&self, img: &AppImg) -> bool {
self.same_name(img, &self.current)
}
fn is_default(&self, img: &AppImg) -> bool {
self.same_name(img, &self.default)
}
fn same_name(&self, img: &AppImg, name: &Option<String>) -> bool {
if let Some(ref name) = *name {
name == img.name()
} else {
false
}
}
pub fn image_exists(&self, name: &str) -> bool {
self.images.contains_key(name)
}
pub fn set_default(&mut self, name: &str) -> Result<()> {
if !is_valid_name(name) {
warn!("{} is not a valid image name", name);
return Ok(())
}
if let Some(ref default) = self.default {
if default == name {
warn!("{} is already default appimg", name);
return Ok(())
}
}
let path = APPIMG_BASE_PATH.join("default.appimg");
if path.exists() {
fs::remove_file(&path)?;
}
symlink(name, &path)?;
self.default = Some(name.to_string());
Ok(())
}
pub fn set_current(&mut self, name: &str) -> Result<()> {
{
let img = match self.images.get(name) {
Some(img) => img,
None => {
warn!("Cannot set {} as current, no image with that name exists", name);
return Ok(());
},
};
if self.is_current(img) {
warn!("Image {} is already current image", name);
return Ok(());
}
if !img.is_running() {
img.start()?;
}
}
self.set_current_target(name)?;
systemd::systemctl_restart(DESKTOPD_SERVICE);
Ok(())
}
fn set_current_target(&mut self, name: &str) -> Result<()> {
if !is_valid_name(name) {
warn!("{} is not a valid image name", name);
return Ok(())
}
if let Some(ref current) = self.current {
if current == name {
warn!("{} is already current appimg", name);
return Ok(())
}
}
fs::create_dir_all(APPIMG_RUN_PATH.as_path())?;
let path = APPIMG_RUN_PATH.join("current.appimg");
let target = APPIMG_BASE_PATH.join(name);
if path.exists() {
fs::remove_file(&path)?;
}
symlink(&target, &path)?;
let script = format!("#!/bin/bash\nmachinectl -E DESKTOP_STARTUP_ID=${{DESKTOP_STARTUP_ID}} shell user@{} /usr/libexec/launch $@\n", name);
let script_path = APPIMG_RUN_PATH.join("run-in-image");
let mut f = fs::OpenOptions::new()
.create(true)
.write(true)
.mode(0o755)
.open(&script_path)?;
f.write_all(script.as_bytes())?;
self.current = Some(name.to_string());
Ok(())
}
}
fn appimg_symlink(symlink: &Path) -> Result<Option<String>> {
if !symlink.exists() {
return Ok(None);
}
if !symlink.symlink_metadata()?.file_type().is_symlink() {
bail!("{} exists but it is not a symlink", symlink.display());
}
let link = fs::read_link(&symlink)?;
let appimg_name = appimg_name_for_symlink_target(&link)?;
if !is_valid_name(&appimg_name) {
bail!("symlink {} points to a directory with a name ({}) that is not a valid appimg name", symlink.display(), appimg_name);
}
Ok(Some(appimg_name))
}
fn default_appimg() -> Result<Option<String>> {
appimg_symlink(&APPIMG_BASE_PATH.join("default.appimg"))
}
fn current_appimg() -> Result<Option<String>> {
appimg_symlink(&APPIMG_RUN_PATH.join("current.appimg"))
}
///
/// Returns a name only if target points to some subdirectory of APPIMG_BASE_PATH
///
fn appimg_name_for_symlink_target(target: &Path) -> Result<String> {
let path = if target.is_absolute() {
target.to_path_buf()
} else if target.components().count() == 1 {
APPIMG_BASE_PATH.join(target)
} else {
bail!("symlink target has invalid value: {}", target.display())
};
match path.parent() {
Some(parent) => {
if parent != APPIMG_BASE_PATH.as_path() {
bail!("symlink target points outside of /storage/appimg directory");
}
},
None => {
bail!("symlink target has invalid value (no parent): {}", target.display())
},
};
if !path.is_dir() {
bail!("symlink target {} is not a directory", path.display());
}
Ok(path_filename(&path).to_string())
}