mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-02-25 12:29:16 -05:00
use xsv-style subcommands like "moonfire-nvr run"
This makes it easier to understand which options are valid with each command. Additionally, there's more separation of implementations. The most obvious consequence is that "moonfire-nvr ts ..." no longer uselessly locks/opens a database.
This commit is contained in:
parent
a6ec68027a
commit
3af9aeee96
@ -268,7 +268,7 @@ been done for you. If not, Create
|
|||||||
After=network-online.target
|
After=network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
ExecStart=/usr/local/bin/moonfire-nvr \
|
ExecStart=/usr/local/bin/moonfire-nvr run \
|
||||||
--sample-file-dir=/var/lib/moonfire-nvr/sample \
|
--sample-file-dir=/var/lib/moonfire-nvr/sample \
|
||||||
--db-dir=/var/lib/moonfire-nvr/db \
|
--db-dir=/var/lib/moonfire-nvr/db \
|
||||||
--http-addr=0.0.0.0:8080
|
--http-addr=0.0.0.0:8080
|
||||||
|
2
prep.sh
2
prep.sh
@ -231,7 +231,7 @@ Description=${SERVICE_DESC}
|
|||||||
After=network-online.target
|
After=network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
ExecStart=${SERVICE_BIN} \\
|
ExecStart=${SERVICE_BIN} run \\
|
||||||
--sample-file-dir=${SAMPLES_PATH} \\
|
--sample-file-dir=${SAMPLES_PATH} \\
|
||||||
--db-dir=${DB_DIR} \\
|
--db-dir=${DB_DIR} \\
|
||||||
--http-addr=0.0.0.0:${NVR_PORT}
|
--http-addr=0.0.0.0:${NVR_PORT}
|
||||||
|
@ -33,10 +33,33 @@
|
|||||||
use db;
|
use db;
|
||||||
use error::Error;
|
use error::Error;
|
||||||
use recording;
|
use recording;
|
||||||
use rusqlite;
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
static USAGE: &'static str = r#"
|
||||||
|
Checks database integrity.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
moonfire-nvr check [options]
|
||||||
|
moonfire-nvr check --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]
|
||||||
|
"#;
|
||||||
|
|
||||||
|
#[derive(Debug, RustcDecodable)]
|
||||||
|
struct Args {
|
||||||
|
flag_db_dir: String,
|
||||||
|
flag_sample_file_dir: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
struct RecordingSummary {
|
struct RecordingSummary {
|
||||||
bytes: u64,
|
bytes: u64,
|
||||||
@ -73,9 +96,12 @@ struct File {
|
|||||||
composite_id: Option<i64>,
|
composite_id: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(conn: rusqlite::Connection, sample_file_dir: &str) -> Result<(), Error> {
|
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, true)?;
|
||||||
let mut files = Vec::new();
|
let mut files = Vec::new();
|
||||||
for e in fs::read_dir(sample_file_dir)? {
|
for e in fs::read_dir(&args.flag_sample_file_dir)? {
|
||||||
let e = e?;
|
let e = e?;
|
||||||
let uuid = match e.file_name().to_str().and_then(|f| Uuid::parse_str(f).ok()) {
|
let uuid = match e.file_name().to_str().and_then(|f| Uuid::parse_str(f).ok()) {
|
||||||
Some(f) => f,
|
Some(f) => f,
|
102
src/cmds/mod.rs
Normal file
102
src/cmds/mod.rs
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||||
|
// Copyright (C) 2016 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/>.
|
||||||
|
|
||||||
|
use dir;
|
||||||
|
use docopt;
|
||||||
|
use error::Error;
|
||||||
|
use libc;
|
||||||
|
use rusqlite;
|
||||||
|
use slog::{self, DrainExt};
|
||||||
|
use slog_envlogger;
|
||||||
|
use slog_stdlog;
|
||||||
|
use slog_term;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
mod check;
|
||||||
|
mod run;
|
||||||
|
mod ts;
|
||||||
|
mod upgrade;
|
||||||
|
|
||||||
|
#[derive(Debug, RustcDecodable)]
|
||||||
|
pub enum Command {
|
||||||
|
Run,
|
||||||
|
Upgrade,
|
||||||
|
Check,
|
||||||
|
Ts,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Command {
|
||||||
|
pub fn run(&self) -> Result<(), Error> {
|
||||||
|
match *self {
|
||||||
|
Command::Run => run::run(),
|
||||||
|
Command::Upgrade => upgrade::run(),
|
||||||
|
Command::Check => check::run(),
|
||||||
|
Command::Ts => ts::run(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initializes logging.
|
||||||
|
/// `async` should be true only for serving; otherwise logging can block useful work.
|
||||||
|
/// 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_envlogger::new(if async { drain.async() } else { drain }.full().build());
|
||||||
|
slog_stdlog::set_logger(slog::Logger::root(drain.ignore_err(), None)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Locks and opens the database.
|
||||||
|
/// The returned `dir::Fd` holds the lock and should be kept open as long as the `Connection` is.
|
||||||
|
fn open_conn(db_dir: &str, read_only: bool) -> Result<(dir::Fd, rusqlite::Connection), Error> {
|
||||||
|
let dir = dir::Fd::open(db_dir)?;
|
||||||
|
dir.lock(if read_only { libc::LOCK_SH } else { libc::LOCK_EX } | libc::LOCK_NB)
|
||||||
|
.map_err(|e| Error{description: format!("db dir {:?} already in use; can't get {} lock",
|
||||||
|
db_dir,
|
||||||
|
if read_only { "shared" } else { "exclusive" }),
|
||||||
|
cause: Some(Box::new(e))})?;
|
||||||
|
let conn = rusqlite::Connection::open_with_flags(
|
||||||
|
Path::new(&db_dir).join("db"),
|
||||||
|
if read_only {
|
||||||
|
rusqlite::SQLITE_OPEN_READ_ONLY
|
||||||
|
} else {
|
||||||
|
rusqlite::SQLITE_OPEN_READ_WRITE
|
||||||
|
} |
|
||||||
|
// rusqlite::Connection is not Sync, so there's no reason to tell SQLite3 to use the
|
||||||
|
// serialized threading mode.
|
||||||
|
rusqlite::SQLITE_OPEN_NO_MUTEX)?;
|
||||||
|
Ok((dir, conn))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_args<T>(usage: &str) -> Result<T, Error> where T: ::rustc_serialize::Decodable {
|
||||||
|
Ok(docopt::Docopt::new(usage)
|
||||||
|
.and_then(|d| d.decode())
|
||||||
|
.unwrap_or_else(|e| e.exit()))
|
||||||
|
}
|
129
src/cmds/run.rs
Normal file
129
src/cmds/run.rs
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||||
|
// Copyright (C) 2016 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/>.
|
||||||
|
|
||||||
|
use chan_signal;
|
||||||
|
use clock;
|
||||||
|
use db;
|
||||||
|
use dir;
|
||||||
|
use error::Error;
|
||||||
|
use hyper::server::Server;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::thread;
|
||||||
|
use stream;
|
||||||
|
use streamer;
|
||||||
|
use web;
|
||||||
|
|
||||||
|
const USAGE: &'static str = r#"
|
||||||
|
Usage: moonfire-nvr run [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h, --help Show this message.
|
||||||
|
--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]
|
||||||
|
--http-addr=ADDR Set the bind address for the unencrypted HTTP server.
|
||||||
|
[default: 0.0.0.0:8080]
|
||||||
|
--read-only Forces read-only mode / disables recording.
|
||||||
|
"#;
|
||||||
|
|
||||||
|
#[derive(Debug, RustcDecodable)]
|
||||||
|
struct Args {
|
||||||
|
flag_db_dir: String,
|
||||||
|
flag_sample_file_dir: String,
|
||||||
|
flag_http_addr: String,
|
||||||
|
flag_read_only: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run() -> Result<(), Error> {
|
||||||
|
let args: Args = super::parse_args(USAGE)?;
|
||||||
|
|
||||||
|
// Watch for termination signals.
|
||||||
|
// This must be started before any threads are spawned (such as the async logger thread) so
|
||||||
|
// that signals will be blocked in all threads.
|
||||||
|
let signal = chan_signal::notify(&[chan_signal::Signal::INT, chan_signal::Signal::TERM]);
|
||||||
|
super::install_logger(true);
|
||||||
|
let (_db_dir, conn) = super::open_conn(&args.flag_db_dir, args.flag_read_only)?;
|
||||||
|
let db = Arc::new(db::Database::new(conn).unwrap());
|
||||||
|
let dir = dir::SampleFileDir::new(&args.flag_sample_file_dir, db.clone()).unwrap();
|
||||||
|
info!("Database is loaded.");
|
||||||
|
|
||||||
|
// Start a streamer for each camera.
|
||||||
|
let shutdown = Arc::new(AtomicBool::new(false));
|
||||||
|
let mut streamers = Vec::new();
|
||||||
|
let syncer = if !args.flag_read_only {
|
||||||
|
let (syncer_channel, syncer_join) = dir::start_syncer(dir.clone()).unwrap();
|
||||||
|
let l = db.lock();
|
||||||
|
let cameras = l.cameras_by_id().len();
|
||||||
|
let env = streamer::Environment{
|
||||||
|
db: &db,
|
||||||
|
dir: &dir,
|
||||||
|
clocks: &clock::REAL,
|
||||||
|
opener: &*stream::FFMPEG,
|
||||||
|
shutdown: &shutdown,
|
||||||
|
};
|
||||||
|
for (i, (id, camera)) in l.cameras_by_id().iter().enumerate() {
|
||||||
|
let rotate_offset_sec = streamer::ROTATE_INTERVAL_SEC * i as i64 / cameras as i64;
|
||||||
|
let mut streamer = streamer::Streamer::new(&env, syncer_channel.clone(), *id, camera,
|
||||||
|
rotate_offset_sec,
|
||||||
|
streamer::ROTATE_INTERVAL_SEC);
|
||||||
|
let name = format!("stream-{}", streamer.short_name());
|
||||||
|
streamers.push(thread::Builder::new().name(name).spawn(move|| {
|
||||||
|
streamer.run();
|
||||||
|
}).expect("can't create thread"));
|
||||||
|
}
|
||||||
|
Some((syncer_channel, syncer_join))
|
||||||
|
} else { None };
|
||||||
|
|
||||||
|
// Start the web interface.
|
||||||
|
let server = Server::http(args.flag_http_addr.as_str()).unwrap();
|
||||||
|
let h = web::Handler::new(db.clone(), dir.clone());
|
||||||
|
let _guard = server.handle(h);
|
||||||
|
info!("Ready to serve HTTP requests");
|
||||||
|
|
||||||
|
// Wait for a signal and shut down.
|
||||||
|
chan_select! {
|
||||||
|
signal.recv() -> signal => info!("Received signal {:?}; shutting down streamers.", signal),
|
||||||
|
}
|
||||||
|
shutdown.store(true, Ordering::SeqCst);
|
||||||
|
for streamer in streamers.drain(..) {
|
||||||
|
streamer.join().unwrap();
|
||||||
|
}
|
||||||
|
if let Some((syncer_channel, syncer_join)) = syncer {
|
||||||
|
info!("Shutting down syncer.");
|
||||||
|
drop(syncer_channel);
|
||||||
|
syncer_join.join().unwrap();
|
||||||
|
}
|
||||||
|
info!("Exiting.");
|
||||||
|
::std::process::exit(0);
|
||||||
|
}
|
52
src/cmds/ts.rs
Normal file
52
src/cmds/ts.rs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||||
|
// Copyright (C) 2016 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/>.
|
||||||
|
|
||||||
|
use error::Error;
|
||||||
|
use recording;
|
||||||
|
|
||||||
|
const USAGE: &'static str = r#"
|
||||||
|
Usage: moonfire-nvr ts <ts>...
|
||||||
|
moonfire-nvr ts --help
|
||||||
|
"#;
|
||||||
|
|
||||||
|
#[derive(Debug, RustcDecodable)]
|
||||||
|
struct Args {
|
||||||
|
arg_ts: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run() -> Result<(), Error> {
|
||||||
|
let arg: Args = super::parse_args(&USAGE)?;
|
||||||
|
super::install_logger(false);
|
||||||
|
for timestamp in &arg.arg_ts {
|
||||||
|
let t = recording::Time::parse(timestamp)?;
|
||||||
|
println!("{} == {}", t, t.0);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -38,6 +38,29 @@ use rusqlite;
|
|||||||
|
|
||||||
mod v0_to_v1;
|
mod v0_to_v1;
|
||||||
|
|
||||||
|
const USAGE: &'static str = r#"
|
||||||
|
Upgrade to the latest database schema.
|
||||||
|
|
||||||
|
Usage: moonfire-nvr upgrade [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h, --help Show this message.
|
||||||
|
--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]
|
||||||
|
--preset-journal=MODE Resets the SQLite journal_mode to the specified mode
|
||||||
|
prior to the upgrade. The default, delete, is
|
||||||
|
recommended. off is very dangerous but may be
|
||||||
|
desirable in some circumstances. See guide/schema.md
|
||||||
|
for more information. The journal mode will be reset
|
||||||
|
to wal after the upgrade.
|
||||||
|
[default: delete]
|
||||||
|
--no-vacuum Skips the normal post-upgrade vacuum operation.
|
||||||
|
"#;
|
||||||
|
|
||||||
const UPGRADE_NOTES: &'static str =
|
const UPGRADE_NOTES: &'static str =
|
||||||
concat!("upgraded using moonfire-nvr ", env!("CARGO_PKG_VERSION"));
|
concat!("upgraded using moonfire-nvr ", env!("CARGO_PKG_VERSION"));
|
||||||
|
|
||||||
@ -45,6 +68,14 @@ const UPGRADERS: [fn(&rusqlite::Transaction) -> Result<(), Error>; 1] = [
|
|||||||
v0_to_v1::run,
|
v0_to_v1::run,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
#[derive(Debug, RustcDecodable)]
|
||||||
|
struct Args {
|
||||||
|
flag_db_dir: String,
|
||||||
|
flag_sample_file_dir: String,
|
||||||
|
flag_preset_journal: String,
|
||||||
|
flag_no_vacuum: bool,
|
||||||
|
}
|
||||||
|
|
||||||
fn set_journal_mode(conn: &rusqlite::Connection, requested: &str) -> Result<(), Error> {
|
fn set_journal_mode(conn: &rusqlite::Connection, requested: &str) -> Result<(), Error> {
|
||||||
assert!(!requested.contains(';')); // quick check for accidental sql injection.
|
assert!(!requested.contains(';')); // quick check for accidental sql injection.
|
||||||
let actual = conn.query_row(&format!("pragma journal_mode = {}", requested), &[],
|
let actual = conn.query_row(&format!("pragma journal_mode = {}", requested), &[],
|
||||||
@ -53,8 +84,11 @@ fn set_journal_mode(conn: &rusqlite::Connection, requested: &str) -> Result<(),
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(mut conn: rusqlite::Connection, preset_journal: &str,
|
pub fn run() -> Result<(), Error> {
|
||||||
no_vacuum: bool) -> Result<(), Error> {
|
let args: Args = super::parse_args(USAGE)?;
|
||||||
|
super::install_logger(false);
|
||||||
|
let (_db_dir, mut conn) = super::open_conn(&args.flag_db_dir, false)?;
|
||||||
|
|
||||||
{
|
{
|
||||||
assert_eq!(UPGRADERS.len(), db::EXPECTED_VERSION as usize);
|
assert_eq!(UPGRADERS.len(), db::EXPECTED_VERSION as usize);
|
||||||
let old_ver =
|
let old_ver =
|
||||||
@ -66,7 +100,7 @@ pub fn run(mut conn: rusqlite::Connection, preset_journal: &str,
|
|||||||
return Err(Error::new(format!("Database is at negative version {}!", old_ver)));
|
return Err(Error::new(format!("Database is at negative version {}!", old_ver)));
|
||||||
}
|
}
|
||||||
info!("Upgrading database from version {} to version {}...", old_ver, db::EXPECTED_VERSION);
|
info!("Upgrading database from version {} to version {}...", old_ver, db::EXPECTED_VERSION);
|
||||||
set_journal_mode(&conn, preset_journal).unwrap();
|
set_journal_mode(&conn, &args.flag_preset_journal).unwrap();
|
||||||
for ver in old_ver .. db::EXPECTED_VERSION {
|
for ver in old_ver .. db::EXPECTED_VERSION {
|
||||||
info!("...from version {} to version {}", ver, ver + 1);
|
info!("...from version {} to version {}", ver, ver + 1);
|
||||||
let tx = conn.transaction()?;
|
let tx = conn.transaction()?;
|
||||||
@ -82,7 +116,7 @@ pub fn run(mut conn: rusqlite::Connection, preset_journal: &str,
|
|||||||
// WAL is the preferred journal mode for normal operation; it reduces the number of syncs
|
// WAL is the preferred journal mode for normal operation; it reduces the number of syncs
|
||||||
// without compromising safety.
|
// without compromising safety.
|
||||||
set_journal_mode(&conn, "wal").unwrap();
|
set_journal_mode(&conn, "wal").unwrap();
|
||||||
if !no_vacuum {
|
if !args.flag_no_vacuum {
|
||||||
info!("...vacuuming database after upgrade.");
|
info!("...vacuuming database after upgrade.");
|
||||||
conn.execute_batch(r#"
|
conn.execute_batch(r#"
|
||||||
pragma page_size = 16384;
|
pragma page_size = 16384;
|
173
src/main.rs
173
src/main.rs
@ -61,16 +61,9 @@ extern crate time;
|
|||||||
extern crate url;
|
extern crate url;
|
||||||
extern crate uuid;
|
extern crate uuid;
|
||||||
|
|
||||||
use hyper::server::Server;
|
|
||||||
use slog::DrainExt;
|
|
||||||
use std::path::Path;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
|
||||||
use std::thread;
|
|
||||||
|
|
||||||
mod check;
|
|
||||||
mod clock;
|
mod clock;
|
||||||
mod coding;
|
mod coding;
|
||||||
|
mod cmds;
|
||||||
mod db;
|
mod db;
|
||||||
mod dir;
|
mod dir;
|
||||||
mod error;
|
mod error;
|
||||||
@ -83,164 +76,54 @@ mod stream;
|
|||||||
mod streamer;
|
mod streamer;
|
||||||
mod strutil;
|
mod strutil;
|
||||||
#[cfg(test)] mod testutil;
|
#[cfg(test)] mod testutil;
|
||||||
mod upgrade;
|
|
||||||
mod web;
|
mod web;
|
||||||
|
|
||||||
/// Commandline usage string. This is in the particular format expected by the `docopt` crate.
|
/// Commandline usage string. This is in the particular format expected by the `docopt` crate.
|
||||||
/// Besides being printed on --help or argument parsing error, it's actually parsed to define the
|
/// Besides being printed on --help or argument parsing error, it's actually parsed to define the
|
||||||
/// allowed commandline arguments and their defaults.
|
/// allowed commandline arguments and their defaults.
|
||||||
const USAGE: &'static str = "
|
const USAGE: &'static str = "
|
||||||
Usage: moonfire-nvr [options]
|
Usage: moonfire-nvr <command> [<args>...]
|
||||||
moonfire-nvr --upgrade [options]
|
|
||||||
moonfire-nvr --check [options]
|
|
||||||
moonfire-nvr --ts <ts>...
|
|
||||||
moonfire-nvr (--help | --version)
|
moonfire-nvr (--help | --version)
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-h, --help Show this message.
|
-h, --help Show this message.
|
||||||
--version Show the version of moonfire-nvr.
|
--version Show the version of moonfire-nvr.
|
||||||
--db-dir=DIR Set the directory holding the SQLite3 index database.
|
|
||||||
This is typically on a flash device.
|
Commands:
|
||||||
[default: /var/lib/moonfire-nvr/db]
|
run Run the daemon: record from cameras and handle HTTP requests
|
||||||
--sample-file-dir=DIR Set the directory holding video data.
|
upgrade Upgrade the database to the latest schema
|
||||||
This is typically on a hard drive.
|
check Check database integrity
|
||||||
[default: /var/lib/moonfire-nvr/sample]
|
ts Translate between human-readable and numeric timestamps
|
||||||
--http-addr=ADDR Set the bind address for the unencrypted HTTP server.
|
|
||||||
[default: 0.0.0.0:8080]
|
|
||||||
--read-only Forces read-only mode / disables recording.
|
|
||||||
--preset-journal=MODE With --upgrade, resets the SQLite journal_mode to
|
|
||||||
the specified mode prior to the upgrade. The default,
|
|
||||||
delete, is recommended. off is very dangerous but
|
|
||||||
may be desirable in some circumstances. See
|
|
||||||
guide/schema.md for more information. The journal
|
|
||||||
mode will be reset to wal after the upgrade.
|
|
||||||
[default: delete]
|
|
||||||
--no-vacuum With --upgrade, skips the normal post-upgrade vacuum
|
|
||||||
operation.
|
|
||||||
";
|
";
|
||||||
|
|
||||||
/// Commandline arguments corresponding to `USAGE`; automatically filled by the `docopt` crate.
|
/// Commandline arguments corresponding to `USAGE`; automatically filled by the `docopt` crate.
|
||||||
#[derive(RustcDecodable)]
|
#[derive(Debug, RustcDecodable)]
|
||||||
struct Args {
|
struct Args {
|
||||||
flag_db_dir: String,
|
arg_command: Option<cmds::Command>,
|
||||||
flag_sample_file_dir: String,
|
}
|
||||||
flag_http_addr: String,
|
|
||||||
flag_read_only: bool,
|
fn version() -> String {
|
||||||
flag_check: bool,
|
let major = option_env!("CARGO_PKG_VERSION_MAJOR");
|
||||||
flag_upgrade: bool,
|
let minor = option_env!("CARGO_PKG_VERSION_MAJOR");
|
||||||
flag_ts: bool,
|
let patch = option_env!("CARGO_PKG_VERSION_MAJOR");
|
||||||
flag_no_vacuum: bool,
|
match (major, minor, patch) {
|
||||||
flag_preset_journal: String,
|
(Some(major), Some(minor), Some(patch)) => format!("{}.{}.{}", major, minor, patch),
|
||||||
arg_ts: Vec<String>,
|
_ => "".to_owned(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// Parse commandline arguments.
|
// Parse commandline arguments.
|
||||||
let version = "Moonfire NVR 0.1.0".to_owned();
|
// (Note this differs from cmds::parse_args in that it specifies options_first.)
|
||||||
let args: Args = docopt::Docopt::new(USAGE)
|
let args: Args = docopt::Docopt::new(USAGE)
|
||||||
.and_then(|d| d.version(Some(version)).decode())
|
.and_then(|d| d.options_first(true)
|
||||||
|
.version(Some(version()))
|
||||||
|
.decode())
|
||||||
.unwrap_or_else(|e| e.exit());
|
.unwrap_or_else(|e| e.exit());
|
||||||
|
|
||||||
// Watch for termination signals.
|
if let Err(e) = args.arg_command.unwrap().run() {
|
||||||
// This must be started before any threads are spawned (such as the async logger thread) so
|
use std::io::Write;
|
||||||
// that signals will be blocked in all threads.
|
writeln!(&mut ::std::io::stderr(), "{}", e).unwrap();
|
||||||
let signal = chan_signal::notify(&[chan_signal::Signal::INT, chan_signal::Signal::TERM]);
|
::std::process::exit(1);
|
||||||
|
|
||||||
// Initialize logging.
|
|
||||||
// Use async logging for serving because otherwise it blocks useful work.
|
|
||||||
// Use sync logging for other modes because async apparently is never flushed before the
|
|
||||||
// program exits, and partial output from these tools is very confusing.
|
|
||||||
let drain = slog_term::StreamerBuilder::new();
|
|
||||||
let drain = slog_envlogger::new(if args.flag_upgrade || args.flag_check { drain }
|
|
||||||
else { drain.async() }.full().build());
|
|
||||||
slog_stdlog::set_logger(slog::Logger::root(drain.ignore_err(), None)).unwrap();
|
|
||||||
|
|
||||||
// Open the database and populate cached state.
|
|
||||||
let db_dir = dir::Fd::open(&args.flag_db_dir).unwrap();
|
|
||||||
db_dir.lock(if args.flag_read_only { libc::LOCK_SH } else { libc::LOCK_EX } | libc::LOCK_NB)
|
|
||||||
.unwrap();
|
|
||||||
let conn = rusqlite::Connection::open_with_flags(
|
|
||||||
Path::new(&args.flag_db_dir).join("db"),
|
|
||||||
if args.flag_read_only {
|
|
||||||
rusqlite::SQLITE_OPEN_READ_ONLY
|
|
||||||
} else {
|
|
||||||
rusqlite::SQLITE_OPEN_READ_WRITE
|
|
||||||
} |
|
|
||||||
// rusqlite::Connection is not Sync, so there's no reason to tell SQLite3 to use the
|
|
||||||
// serialized threading mode.
|
|
||||||
rusqlite::SQLITE_OPEN_NO_MUTEX).unwrap();
|
|
||||||
|
|
||||||
if args.flag_upgrade {
|
|
||||||
upgrade::run(conn, &args.flag_preset_journal, args.flag_no_vacuum).unwrap();
|
|
||||||
} else if args.flag_check {
|
|
||||||
check::run(conn, &args.flag_sample_file_dir).unwrap();
|
|
||||||
} else if args.flag_ts {
|
|
||||||
run_ts(args.arg_ts).unwrap();
|
|
||||||
} else {
|
|
||||||
run(args, conn, &signal);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_ts(timestamps: Vec<String>) -> Result<(), error::Error> {
|
|
||||||
for timestamp in ×tamps {
|
|
||||||
let t = recording::Time::parse(timestamp)?;
|
|
||||||
println!("{} == {}", t, t.0);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run(args: Args, conn: rusqlite::Connection, signal: &chan::Receiver<chan_signal::Signal>) {
|
|
||||||
let db = Arc::new(db::Database::new(conn).unwrap());
|
|
||||||
let dir = dir::SampleFileDir::new(&args.flag_sample_file_dir, db.clone()).unwrap();
|
|
||||||
info!("Database is loaded.");
|
|
||||||
|
|
||||||
// Start a streamer for each camera.
|
|
||||||
let shutdown = Arc::new(AtomicBool::new(false));
|
|
||||||
let mut streamers = Vec::new();
|
|
||||||
let syncer = if !args.flag_read_only {
|
|
||||||
let (syncer_channel, syncer_join) = dir::start_syncer(dir.clone()).unwrap();
|
|
||||||
let l = db.lock();
|
|
||||||
let cameras = l.cameras_by_id().len();
|
|
||||||
let env = streamer::Environment{
|
|
||||||
db: &db,
|
|
||||||
dir: &dir,
|
|
||||||
clocks: &clock::REAL,
|
|
||||||
opener: &*stream::FFMPEG,
|
|
||||||
shutdown: &shutdown,
|
|
||||||
};
|
|
||||||
for (i, (id, camera)) in l.cameras_by_id().iter().enumerate() {
|
|
||||||
let rotate_offset_sec = streamer::ROTATE_INTERVAL_SEC * i as i64 / cameras as i64;
|
|
||||||
let mut streamer = streamer::Streamer::new(&env, syncer_channel.clone(), *id, camera,
|
|
||||||
rotate_offset_sec,
|
|
||||||
streamer::ROTATE_INTERVAL_SEC);
|
|
||||||
let name = format!("stream-{}", streamer.short_name());
|
|
||||||
streamers.push(thread::Builder::new().name(name).spawn(move|| {
|
|
||||||
streamer.run();
|
|
||||||
}).expect("can't create thread"));
|
|
||||||
}
|
|
||||||
Some((syncer_channel, syncer_join))
|
|
||||||
} else { None };
|
|
||||||
|
|
||||||
// Start the web interface.
|
|
||||||
let server = Server::http(args.flag_http_addr.as_str()).unwrap();
|
|
||||||
let h = web::Handler::new(db.clone(), dir.clone());
|
|
||||||
let _guard = server.handle(h);
|
|
||||||
info!("Ready to serve HTTP requests");
|
|
||||||
|
|
||||||
// Wait for a signal and shut down.
|
|
||||||
chan_select! {
|
|
||||||
signal.recv() -> signal => info!("Received signal {:?}; shutting down streamers.", signal),
|
|
||||||
}
|
|
||||||
shutdown.store(true, Ordering::SeqCst);
|
|
||||||
for streamer in streamers.drain(..) {
|
|
||||||
streamer.join().unwrap();
|
|
||||||
}
|
|
||||||
if let Some((syncer_channel, syncer_join)) = syncer {
|
|
||||||
info!("Shutting down syncer.");
|
|
||||||
drop(syncer_channel);
|
|
||||||
syncer_join.join().unwrap();
|
|
||||||
}
|
|
||||||
info!("Exiting.");
|
|
||||||
// TODO: drain the logger.
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user