new "moonfire-nvr config" subcommand

This is a ncurses-based user interface for configuration. This fills a major
usability gap: the system can be configured without manual SQL commands.
This commit is contained in:
Scott Lamb
2017-02-05 19:58:41 -08:00
parent b3a7795407
commit c82f038bef
11 changed files with 1079 additions and 133 deletions

270
src/cmds/config/cameras.rs Normal file
View File

@@ -0,0 +1,270 @@
// This file is part of Moonfire NVR, a security camera digital video recorder.
// Copyright (C) 2017 Scott Lamb <slamb@slamb.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// In addition, as a special exception, the copyright holders give
// permission to link the code of portions of this program with the
// OpenSSL library under certain conditions as described in each
// individual source file, and distribute linked combinations including
// the two.
//
// You must obey the GNU General Public License in all respects for all
// of the code used other than OpenSSL. If you modify file(s) with this
// exception, you may extend this exception to your version of the
// file(s), but you are not obligated to do so. If you do not wish to do
// so, delete this exception statement from your version. If you delete
// this exception statement from all source files in the program, then
// also delete it here.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
extern crate cursive;
use self::cursive::Cursive;
use self::cursive::traits::{Boxable, Identifiable, Finder};
use self::cursive::views;
use db;
use dir;
use error::Error;
use std::sync::Arc;
use stream::{self, Opener, Stream};
use super::{decode_size, encode_size};
/// Builds a `CameraChange` from an active `edit_camera_dialog`.
fn get_change(siv: &mut Cursive) -> db::CameraChange {
db::CameraChange{
short_name: siv.find_id::<views::EditView>("short_name")
.unwrap().get_content().as_str().into(),
description: siv.find_id::<views::TextArea>("description").unwrap().get_content().into(),
host: siv.find_id::<views::EditView>("host").unwrap().get_content().as_str().into(),
username: siv.find_id::<views::EditView>("username").unwrap().get_content().as_str().into(),
password: siv.find_id::<views::EditView>("password").unwrap().get_content().as_str().into(),
main_rtsp_path: siv.find_id::<views::EditView>("main_rtsp_path")
.unwrap().get_content().as_str().into(),
sub_rtsp_path: siv.find_id::<views::EditView>("sub_rtsp_path")
.unwrap().get_content().as_str().into(),
}
}
fn press_edit(siv: &mut Cursive, db: &Arc<db::Database>, dir: &Arc<dir::SampleFileDir>,
id: Option<i32>) {
let change = get_change(siv);
siv.pop_layer(); // get rid of the add/edit camera dialog.
let result = {
let mut l = db.lock();
if let Some(id) = id {
l.update_camera(id, change)
} else {
l.add_camera(change).map(|_| ())
}
};
if let Err(e) = result {
siv.add_layer(views::Dialog::text(format!("Unable to add camera: {}", e))
.title("Error")
.dismiss_button("Abort"));
} else {
// Recreate the "Edit cameras" dialog from scratch; it's easier than adding the new entry.
siv.pop_layer();
add_dialog(db, dir, siv);
}
}
fn press_test_inner(url: &str) -> Result<String, Error> {
let stream = stream::FFMPEG.open(stream::Source::Rtsp(url))?;
let extra_data = stream.get_extra_data()?;
Ok(format!("{}x{} video stream", extra_data.width, extra_data.height))
}
fn press_test(siv: &mut Cursive, c: &db::CameraChange, stream: &str, path: &str) {
let url = format!("rtsp://{}:{}@{}{}", c.username, c.password, c.host, path);
let description = match press_test_inner(&url) {
Err(e) => {
siv.add_layer(
views::Dialog::text(format!("{} stream at {}:\n\n{}", stream, url, e))
.title("Stream test failed")
.dismiss_button("Back"));
return;
},
Ok(d) => d,
};
siv.add_layer(views::Dialog::text(format!("{} stream at {}:\n\n{}", stream, url, description))
.title("Stream test succeeded")
.dismiss_button("Back"));
}
fn press_delete(siv: &mut Cursive, db: &Arc<db::Database>, dir: &Arc<dir::SampleFileDir>, id: i32,
name: String, to_delete: i64) {
let dialog = if to_delete > 0 {
let prompt = format!("Camera {} has recorded video. Please confirm the amount \
of data to delete by typing it back:\n\n{}", name,
encode_size(to_delete));
views::Dialog::around(
views::LinearLayout::vertical()
.child(views::TextView::new(prompt))
.child(views::DummyView)
.child(views::EditView::new().on_submit({
let db = db.clone();
let dir = dir.clone();
move |siv, _| confirm_deletion(siv, &db, &dir, id, to_delete)
}).with_id("confirm")))
.button("Delete", {
let db = db.clone();
let dir = dir.clone();
move |siv| confirm_deletion(siv, &db, &dir, id, to_delete)
})
} else {
views::Dialog::text(format!("Delete camera {}? This camera has no recorded video.", name))
.button("Delete", {
let db = db.clone();
let dir = dir.clone();
move |s| actually_delete(s, &db, &dir, id)
})
}.title("Delete camera").dismiss_button("Cancel");
siv.add_layer(dialog);
}
fn confirm_deletion(siv: &mut Cursive, db: &Arc<db::Database>, dir: &Arc<dir::SampleFileDir>,
id: i32, to_delete: i64) {
let typed = siv.find_id::<views::EditView>("confirm").unwrap().get_content();
if decode_size(typed.as_str()).ok() == Some(to_delete) {
siv.pop_layer(); // deletion confirmation dialog
if let Err(e) = dir::lower_retention(dir.clone(),
&[dir::NewLimit{camera_id: id, limit: 0}]) {
siv.add_layer(views::Dialog::text(format!("Unable to delete recordings: {}", e))
.title("Error")
.dismiss_button("Abort"));
return;
}
actually_delete(siv, db, dir, id);
} else {
siv.add_layer(views::Dialog::text("Please confirm amount.")
.title("Try again")
.dismiss_button("Back"));
}
}
fn actually_delete(siv: &mut Cursive, db: &Arc<db::Database>, dir: &Arc<dir::SampleFileDir>,
id: i32) {
info!("actually_delete call");
siv.pop_layer(); // get rid of the add/edit camera dialog.
let result = {
let mut l = db.lock();
l.delete_camera(id)
};
if let Err(e) = result {
siv.add_layer(views::Dialog::text(format!("Unable to delete camera: {}", e))
.title("Error")
.dismiss_button("Abort"));
} else {
// Recreate the "Edit cameras" dialog from scratch; it's easier than adding the new entry.
siv.pop_layer();
add_dialog(db, dir, siv);
}
}
/// Adds or updates a camera.
/// (The former if `item` is None; the latter otherwise.)
fn edit_camera_dialog(db: &Arc<db::Database>, dir: &Arc<dir::SampleFileDir>, siv: &mut Cursive,
item: &Option<i32>) {
let list = views::ListView::new()
.child("id", views::TextView::new(match *item {
None => "<new>".to_string(),
Some(id) => id.to_string(),
}))
.child("uuid", views::TextView::new("<new>").with_id("uuid"))
.child("short name", views::EditView::new().with_id("short_name"))
.child("host", views::EditView::new().with_id("host"))
.child("username", views::EditView::new().with_id("username"))
.child("password", views::EditView::new().with_id("password"))
.child("main_rtsp_path", views::LinearLayout::horizontal()
.child(views::EditView::new().with_id("main_rtsp_path").full_width())
.child(views::DummyView)
.child(views::Button::new("Test", |siv| {
let c = get_change(siv);
press_test(siv, &c, "main", &c.main_rtsp_path)
})))
.child("sub_rtsp_path", views::LinearLayout::horizontal()
.child(views::EditView::new().with_id("sub_rtsp_path").full_width())
.child(views::DummyView)
.child(views::Button::new("Test", |siv| {
let c = get_change(siv);
press_test(siv, &c, "sub", &c.sub_rtsp_path)
})))
.min_height(8);
let layout = views::LinearLayout::vertical()
.child(list)
.child(views::TextView::new("description"))
.child(views::TextArea::new().with_id("description").min_height(3))
.full_width();
let mut dialog = views::Dialog::around(layout);
let dialog = if let Some(id) = *item {
let l = db.lock();
let camera = l.cameras_by_id().get(&id).expect("missing camera");
dialog.find_id::<views::TextView>("uuid")
.expect("missing TextView")
.set_content(camera.uuid.to_string());
let bytes = camera.sample_file_bytes;
let name = camera.short_name.clone();
for &(view_id, content) in &[("short_name", &camera.short_name),
("host", &camera.host),
("username", &camera.username),
("password", &camera.password),
("main_rtsp_path", &camera.main_rtsp_path),
("sub_rtsp_path", &camera.sub_rtsp_path)] {
dialog.find_id::<views::EditView>(view_id)
.expect("missing EditView")
.set_content(content.to_string());
}
dialog.find_id::<views::TextArea>("description")
.expect("missing TextArea")
.set_content(camera.description.to_string());
dialog.title("Edit camera")
.button("Edit", {
let db = db.clone();
let dir = dir.clone();
move |s| press_edit(s, &db, &dir, Some(id))
})
.button("Delete", {
let db = db.clone();
let dir = dir.clone();
move |s| press_delete(s, &db, &dir, id, name.clone(), bytes)
})
} else {
dialog.title("Add camera")
.button("Add", {
let db = db.clone();
let dir = dir.clone();
move |s| press_edit(s, &db, &dir, None)
})
};
siv.add_layer(dialog.dismiss_button("Cancel"));
}
pub fn add_dialog(db: &Arc<db::Database>, dir: &Arc<dir::SampleFileDir>, siv: &mut Cursive) {
siv.add_layer(views::Dialog::around(
views::SelectView::new()
.on_submit({
let db = db.clone();
let dir = dir.clone();
move |siv, item| edit_camera_dialog(&db, &dir, siv, item)
})
.item("<new camera>".to_string(), None)
.with_all(db.lock()
.cameras_by_id()
.iter()
.map(|(&id, camera)| (format!("{}: {}", id, camera.short_name), Some(id))))
.full_width())
.dismiss_button("Done")
.title("Edit cameras"));
}

163
src/cmds/config/mod.rs Normal file
View File

@@ -0,0 +1,163 @@
// This file is part of Moonfire NVR, a security camera digital video recorder.
// Copyright (C) 2017 Scott Lamb <slamb@slamb.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// In addition, as a special exception, the copyright holders give
// permission to link the code of portions of this program with the
// OpenSSL library under certain conditions as described in each
// individual source file, and distribute linked combinations including
// the two.
//
// You must obey the GNU General Public License in all respects for all
// of the code used other than OpenSSL. If you modify file(s) with this
// exception, you may extend this exception to your version of the
// file(s), but you are not obligated to do so. If you do not wish to do
// so, delete this exception statement from your version. If you delete
// this exception statement from all source files in the program, then
// also delete it here.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! Text-based configuration interface.
//!
//! This code is a bit messy, but it's essentially a prototype. Eventually Moonfire NVR's
//! configuration will likely be almost entirely done through a web-based UI.
extern crate cursive;
use self::cursive::Cursive;
use self::cursive::views;
use db;
use dir;
use error::Error;
use regex::Regex;
use std::sync::Arc;
use std::fmt::Write;
use std::str::FromStr;
mod cameras;
mod retention;
static USAGE: &'static str = r#"
Interactive configuration editor.
Usage:
moonfire-nvr config [options]
moonfire-nvr config --help
Options:
--db-dir=DIR Set the directory holding the SQLite3 index database.
This is typically on a flash device.
[default: /var/lib/moonfire-nvr/db]
--sample-file-dir=DIR Set the directory holding video data.
This is typically on a hard drive.
[default: /var/lib/moonfire-nvr/sample]
"#;
static MULTIPLIERS: [(char, u64); 4] = [
// (suffix character, power of 2)
('T', 40),
('G', 30),
('M', 20),
('K', 10),
];
fn encode_size(mut raw: i64) -> String {
let mut encoded = String::new();
for &(c, n) in &MULTIPLIERS {
if raw >= 1i64<<n {
write!(&mut encoded, "{}{} ", raw >> n, c).unwrap();
raw &= (1i64 << n) - 1;
}
}
if raw > 0 || encoded.len() == 0 {
write!(&mut encoded, "{}", raw).unwrap();
} else {
encoded.pop(); // remove trailing space.
}
encoded
}
fn decode_size(encoded: &str) -> Result<i64, ()> {
let mut decoded = 0i64;
lazy_static! {
static ref RE: Regex = Regex::new(r"\s*([0-9]+)([TGMK])?,?\s*").unwrap();
}
let mut last_pos = 0;
for cap in RE.captures_iter(encoded) {
let whole_cap = cap.get(0).unwrap();
if whole_cap.start() > last_pos {
return Err(());
}
last_pos = whole_cap.end();
let mut piece = i64::from_str(&cap[1]).map_err(|_| ())?;
if let Some(m) = cap.get(2) {
let m = m.as_str().as_bytes()[0] as char;
for &(some_m, n) in &MULTIPLIERS {
if some_m == m {
piece *= 1i64<<n;
break;
}
}
}
decoded += piece;
}
if last_pos < encoded.len() {
return Err(());
}
Ok(decoded)
}
#[derive(Debug, RustcDecodable)]
struct Args {
flag_db_dir: String,
flag_sample_file_dir: String,
}
pub fn run() -> Result<(), Error> {
let args: Args = super::parse_args(USAGE)?;
super::install_logger(false);
let (_db_dir, conn) = super::open_conn(&args.flag_db_dir, super::OpenMode::ReadWrite)?;
let db = Arc::new(db::Database::new(conn)?);
//let dir = Arc::new(dir::Fd::open(&args.flag_sample_file_dir)?);
let dir = dir::SampleFileDir::new(&args.flag_sample_file_dir, db.clone())?;
let mut siv = Cursive::new();
//siv.add_global_callback('q', |s| s.quit());
siv.add_layer(views::Dialog::around(
views::SelectView::<fn(&Arc<db::Database>, &Arc<dir::SampleFileDir>, &mut Cursive)>::new()
.on_submit({
let db = db.clone();
let dir = dir.clone();
move |siv, item| item(&db, &dir, siv)
})
.item("Edit cameras".to_string(), cameras::add_dialog)
.item("Edit retention".to_string(), retention::add_dialog))
.button("Quit", |siv| siv.quit())
.title("Main menu"));
siv.run();
Ok(())
}
#[cfg(test)]
mod tests {
#[test]
fn test_decode() {
assert_eq!(super::decode_size("100M").unwrap(), 100i64 << 20);
}
}

View File

@@ -0,0 +1,265 @@
// This file is part of Moonfire NVR, a security camera digital video recorder.
// Copyright (C) 2017 Scott Lamb <slamb@slamb.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// In addition, as a special exception, the copyright holders give
// permission to link the code of portions of this program with the
// OpenSSL library under certain conditions as described in each
// individual source file, and distribute linked combinations including
// the two.
//
// You must obey the GNU General Public License in all respects for all
// of the code used other than OpenSSL. If you modify file(s) with this
// exception, you may extend this exception to your version of the
// file(s), but you are not obligated to do so. If you do not wish to do
// so, delete this exception statement from your version. If you delete
// this exception statement from all source files in the program, then
// also delete it here.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
extern crate cursive;
use self::cursive::Cursive;
use self::cursive::traits::{Boxable, Identifiable};
use self::cursive::views;
use db;
use dir;
use error::Error;
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;
use std::sync::Arc;
use super::{decode_size, encode_size};
struct Camera {
label: String,
used: i64,
retain: Option<i64>, // None if unparseable
}
struct Model {
db: Arc<db::Database>,
dir: Arc<dir::SampleFileDir>,
fs_capacity: i64,
total_used: i64,
total_retain: i64,
errors: isize,
cameras: BTreeMap<i32, Camera>,
}
/// Updates the limits in the database. Doesn't delete excess data (if any).
fn update_limits_inner(model: &Model) -> Result<(), Error> {
let mut db = model.db.lock();
let mut tx = db.tx()?;
for (&id, camera) in &model.cameras {
tx.update_retention(id, camera.retain.unwrap())?;
}
tx.commit()
}
fn update_limits(model: &Model, siv: &mut Cursive) {
if let Err(e) = update_limits_inner(model) {
siv.add_layer(views::Dialog::text(format!("Unable to update limits: {}", e))
.dismiss_button("Back")
.title("Error"));
}
}
fn edit_limit(model: &RefCell<Model>, siv: &mut Cursive, id: i32, content: &str) {
info!("on_edit called for id {}", id);
let mut model = model.borrow_mut();
let model: &mut Model = &mut *model;
let camera = model.cameras.get_mut(&id).unwrap();
let new_value = decode_size(content).ok();
let delta = new_value.unwrap_or(0) - camera.retain.unwrap_or(0);
let old_errors = model.errors;
if delta != 0 {
let prev_over = model.total_retain > model.fs_capacity;
model.total_retain += delta;
siv.find_id::<views::TextView>("total_retain")
.unwrap()
.set_content(encode_size(model.total_retain));
let now_over = model.total_retain > model.fs_capacity;
info!("now_over: {}", now_over);
if now_over != prev_over {
model.errors += if now_over { 1 } else { -1 };
siv.find_id::<views::TextView>("total_ok")
.unwrap()
.set_content(if now_over { "*" } else { " " });
}
}
if new_value.is_none() != camera.retain.is_none() {
model.errors += if new_value.is_none() { 1 } else { -1 };
siv.find_id::<views::TextView>(&format!("{}_ok", id))
.unwrap()
.set_content(if new_value.is_none() { "*" } else { " " });
}
camera.retain = new_value;
info!("model.errors = {}", model.errors);
if (model.errors == 0) != (old_errors == 0) {
info!("toggling change state: errors={}", model.errors);
siv.find_id::<views::Button>("change")
.unwrap()
.set_enabled(model.errors == 0);
}
}
fn confirm_deletion(model: &RefCell<Model>, siv: &mut Cursive, to_delete: i64) {
let typed = siv.find_id::<views::EditView>("confirm")
.unwrap()
.get_content();
info!("confirm, typed: {} vs expected: {}", typed.as_str(), to_delete);
if decode_size(typed.as_str()).ok() == Some(to_delete) {
actually_delete(model, siv);
} else {
siv.add_layer(views::Dialog::text("Please confirm amount.")
.title("Try again")
.dismiss_button("Back"));
}
}
fn actually_delete(model: &RefCell<Model>, siv: &mut Cursive) {
let model = &*model.borrow();
let new_limits: Vec<_> =
model.cameras.iter()
.map(|(&id, c)| dir::NewLimit{camera_id: id, limit: c.retain.unwrap()})
.collect();
siv.pop_layer(); // deletion confirmation
siv.pop_layer(); // retention dialog
if let Err(e) = dir::lower_retention(model.dir.clone(), &new_limits[..]) {
siv.add_layer(views::Dialog::text(format!("Unable to delete excess video: {}", e))
.title("Error")
.dismiss_button("Abort"));
} else {
update_limits(model, siv);
}
}
fn press_change(model: &Rc<RefCell<Model>>, siv: &mut Cursive) {
if model.borrow().errors > 0 {
return;
}
let to_delete = model.borrow().cameras.values().map(
|c| ::std::cmp::max(c.used - c.retain.unwrap(), 0)).sum();
info!("change press, to_delete={}", to_delete);
if to_delete > 0 {
let prompt = format!("Some cameras' usage exceeds new limit. Please confirm the amount \
of data to delete by typing it back:\n\n{}", encode_size(to_delete));
let dialog = views::Dialog::around(
views::LinearLayout::vertical()
.child(views::TextView::new(prompt))
.child(views::DummyView)
.child(views::EditView::new().on_submit({
let model = model.clone();
move |siv, _| confirm_deletion(&model, siv, to_delete)
}).with_id("confirm")))
.button("Confirm", {
let model = model.clone();
move |siv| confirm_deletion(&model, siv, to_delete)
})
.dismiss_button("Cancel")
.title("Confirm deletion");
siv.add_layer(dialog);
} else {
siv.screen_mut().pop_layer();
update_limits(&model.borrow(), siv);
}
}
pub fn add_dialog(db: &Arc<db::Database>, dir: &Arc<dir::SampleFileDir>, siv: &mut Cursive) {
let model = {
let mut cameras = BTreeMap::new();
let mut total_used = 0;
let mut total_retain = 0;
{
let db = db.lock();
for (&id, camera) in db.cameras_by_id() {
cameras.insert(id, Camera{
label: format!("{}: {}", id, camera.short_name),
used: camera.sample_file_bytes,
retain: Some(camera.retain_bytes),
});
total_used += camera.sample_file_bytes;
total_retain += camera.retain_bytes;
}
}
let stat = dir.statfs().unwrap();
let fs_capacity = (stat.f_bsize * (stat.f_blocks - stat.f_bfree + stat.f_bavail)) as i64 -
total_used;
Rc::new(RefCell::new(Model{
dir: dir.clone(),
db: db.clone(),
fs_capacity: fs_capacity,
total_used: total_used,
total_retain: total_retain,
errors: (total_retain > fs_capacity) as isize,
cameras: cameras,
}))
};
let mut list = views::ListView::new();
list.add_child(
"camera",
views::LinearLayout::horizontal()
.child(views::TextView::new("usage").fixed_width(25))
.child(views::TextView::new("limit").fixed_width(25)));
for (&id, camera) in &model.borrow().cameras {
list.add_child(
&camera.label,
views::LinearLayout::horizontal()
.child(views::TextView::new(encode_size(camera.used)).fixed_width(25))
.child(views::EditView::new()
.content(encode_size(camera.retain.unwrap()))
.on_edit({
let model = model.clone();
move |siv, content, _pos| edit_limit(&model, siv, id, content)
})
.on_submit({
let model = model.clone();
move |siv, _| press_change(&model, siv)
})
.fixed_width(25))
.child(views::TextView::new("").with_id(&format!("{}_ok", id)).fixed_width(1)));
}
let over = model.borrow().total_retain > model.borrow().fs_capacity;
list.add_child(
"total",
views::LinearLayout::horizontal()
.child(views::TextView::new(encode_size(model.borrow().total_used)).fixed_width(25))
.child(views::TextView::new(encode_size(model.borrow().total_retain))
.with_id("total_retain").fixed_width(25))
.child(views::TextView::new(if over { "*" } else { " " }).with_id("total_ok")));
list.add_child(
"filesystem",
views::LinearLayout::horizontal()
.child(views::TextView::new("").fixed_width(25))
.child(views::TextView::new(encode_size(model.borrow().fs_capacity)).fixed_width(25)));
let mut change_button = views::Button::new("Change", {
let model = model.clone();
move |siv| press_change(&model, siv)
});
change_button.set_enabled(!over);
let mut buttons = views::LinearLayout::horizontal()
.child(views::DummyView.full_width());
buttons.add_child(change_button.with_id("change"));
buttons.add_child(views::DummyView);
buttons.add_child(views::Button::new("Cancel", |siv| siv.screen_mut().pop_layer()));
siv.add_layer(
views::Dialog::around(
views::LinearLayout::vertical()
.child(list)
.child(views::DummyView)
.child(buttons))
.title("Edit retention"));
}

View File

@@ -40,6 +40,7 @@ use slog_term;
use std::path::Path;
mod check;
mod config;
mod init;
mod run;
mod ts;
@@ -48,6 +49,7 @@ mod upgrade;
#[derive(Debug, RustcDecodable)]
pub enum Command {
Check,
Config,
Init,
Run,
Ts,
@@ -58,6 +60,7 @@ impl Command {
pub fn run(&self) -> Result<(), Error> {
match *self {
Command::Check => check::run(),
Command::Config => config::run(),
Command::Init => init::run(),
Command::Run => run::run(),
Command::Ts => ts::run(),
@@ -71,7 +74,7 @@ impl Command {
/// Sync logging should be preferred for other modes because async apparently is never flushed
/// before the program exits, and partial output from these tools is very confusing.
fn install_logger(async: bool) {
let drain = slog_term::StreamerBuilder::new();
let drain = slog_term::StreamerBuilder::new().stderr();
let drain = slog_envlogger::new(if async { drain.async() } else { drain }.full().build());
slog_stdlog::set_logger(slog::Logger::root(drain.ignore_err(), None)).unwrap();
}