diff --git a/CMakeLists.txt b/CMakeLists.txt index 3c1eee1..4975fcc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,10 +20,10 @@ add_executable(mow src/main.cpp src/common.h src/common.cpp - src/logger.h - src/logger.cpp src/camera.h src/camera.cpp + src/services.h + src/services.cpp ) target_link_libraries(mow Qt${QT_VERSION_MAJOR}::Core ${OpenCV_LIBS}) diff --git a/imgmagick_build.sh b/imgmagick_build.sh index fd70d82..a8eeee2 100644 --- a/imgmagick_build.sh +++ b/imgmagick_build.sh @@ -1,9 +1,7 @@ #!/bin/sh export DEBIAN_FRONTEND=noninteractive -magick -version &> /dev/null - -if [ ! $? -eq 0 ] +if [ ! -f "/usr/local/bin/magick" ] then apt install -y git git clone https://github.com/ImageMagick/ImageMagick.git .build-imagemagick diff --git a/install.sh b/install.sh index 502e790..af4b829 100644 --- a/install.sh +++ b/install.sh @@ -19,10 +19,6 @@ if [ ! -d "/var/buffer" ]; then mkdir /var/buffer fi -if [ ! -d "/var/mow_serv" ]; then - mkdir /var/mow_serv -fi - cp -v ./.build-mow/mow /opt/mow/bin echo "writing /opt/mow/run" @@ -31,6 +27,7 @@ printf "/opt/mow/bin \$1 \$2 \$3\n" >> /opt/mow/run echo "writing /opt/mow/uninst" printf "#!/bin/sh\n" > /opt/mow/uninst +printf "mow -r\n" >> /opt/mow/uninst printf "rm -v /opt/mow/bin\n" >> /opt/mow/uninst printf "rm -v /opt/mow/run\n" >> /opt/mow/uninst printf "rm -v /opt/mow/uninst\n" >> /opt/mow/uninst @@ -39,10 +36,10 @@ printf "rm -rv /opt/mow\n" >> /opt/mow/uninst printf "deluser mow\n" >> /opt/mow/uninst useradd -r mow +usermod -aG video mow -chown -R -v mow:mow /var/footage -chown -R -v mow:mow /var/buffer -chown -R -v mow:mow /var/mow_serv +chown -R mow:mow /var/footage +chown -R mow:mow /var/buffer chmod -v +x /opt/mow/run chmod -v +x /opt/mow/bin diff --git a/src/camera.cpp b/src/camera.cpp index 17370d7..a305e25 100644 --- a/src/camera.cpp +++ b/src/camera.cpp @@ -21,17 +21,14 @@ int Camera::start(const QStringList &args) auto thr1 = new QThread(nullptr); auto thr2 = new QThread(nullptr); auto thr3 = new QThread(nullptr); - auto thr4 = new QThread(nullptr); - - new RecLoop(&shared, thr1, nullptr); - new Upkeep(&shared, thr2, nullptr); - new EventLoop(&shared, thr3, nullptr); - new DetectLoop(&shared, thr4, nullptr); + new Upkeep(&shared, thr1, nullptr); + new EventLoop(&shared, thr2, nullptr); + new DetectLoop(&shared, thr3, nullptr); + thr1->start(); thr2->start(); thr3->start(); - thr4->start(); } return shared.retCode; @@ -78,141 +75,10 @@ bool Loop::exec() return shared->retCode == 0; } -RecLoop::RecLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent) -{ - recProc = 0; - imgProc = 0; -} - -void RecLoop::init() -{ - recProc = new QProcess(this); - imgProc = new QProcess(this); - - connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &RecLoop::term); - connect(recProc, &QProcess::readyReadStandardError, this, &RecLoop::rdProcErr); - connect(imgProc, &QProcess::readyReadStandardError, this, &RecLoop::rdProcErr); - - Loop::init(); -} - -void RecLoop::updateCmd() -{ - QStringList recArgs; - QStringList imgArgs; - - recArgs << "-c"; - recArgs << buildThreadCount(shared->recThreads); - recArgs << "ffmpeg"; - recArgs << "-hide_banner"; - recArgs << "-i" << shared->recordUri; - recArgs << "-strftime" << "1"; - recArgs << "-strftime_mkdir" << "1"; - recArgs << "-vf" << "fps=" + QString::number(shared->recFps) + ",scale=" + shared->recScale; - recArgs << "-hls_segment_filename" << "live/" + QString(STRFTIME_FMT) + shared->streamExt; - recArgs << "-y"; - recArgs << "-vcodec" << shared->streamCodec; - recArgs << "-f" << "hls"; - recArgs << "-hls_time" << "2"; - recArgs << "-hls_list_size" << "1000"; - recArgs << "-hls_flags" << "append_list+omit_endlist"; - - if (shared->recordUri.contains("rtsp")) - { - recArgs << "-rtsp_transport" << "tcp"; - } - - recArgs << "-t" << QString::number(heartBeat); - recArgs << "stream.m3u8"; - - imgArgs << "-c"; - imgArgs << buildThreadCount(shared->imgThreads); - imgArgs << "ffmpeg"; - imgArgs << "-hide_banner"; - imgArgs << "-i" << shared->recordUri; - imgArgs << "-strftime" << "1"; - imgArgs << "-strftime_mkdir" << "1"; - imgArgs << "-vf" << "fps=1,scale=" + shared->imgScale; - - if (shared->recordUri.contains("rtsp")) - { - imgArgs << "-rtsp_transport" << "tcp"; - } - - imgArgs << "-t" << QString::number(heartBeat); - imgArgs << "img/" + QString(STRFTIME_FMT) + ".bmp"; - - recProc->setWorkingDirectory(shared->tmpDir); - recProc->setProgram("taskset"); - recProc->setArguments(recArgs); - - imgProc->setWorkingDirectory(shared->tmpDir); - imgProc->setProgram("taskset"); - imgProc->setArguments(imgArgs); - - recLog("rec_args: " + recArgs.join(" "), shared); - recLog("img_args: " + imgArgs.join(" "), shared); -} - -void RecLoop::rdProcErr() -{ - procError("img", imgProc); - procError("rec", recProc); -} - -void RecLoop::term() -{ - recProc->kill(); - recProc->waitForFinished(); - - imgProc->kill(); - imgProc->waitForFinished(); -} - -void RecLoop::procError(const QString &desc, QProcess *proc) -{ - auto errBlob = QString(proc->readAllStandardError()); - auto errLines = errBlob.split('\n'); - - if (!errLines.isEmpty()) - { - for (auto &&line : errLines) - { - recLog(desc + "_cmd_stderr: " + line, shared); - } - } -} - -bool RecLoop::exec() -{ - if ((imgProc->state() == QProcess::Running) || (recProc->state() == QProcess::Running)) - { - term(); - } - - updateCmd(); - - imgProc->start(); - recProc->start(); - - return Loop::exec(); -} - Upkeep::Upkeep(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent) {} bool Upkeep::exec() { - enforceMaxLogSize(shared->tmpDir + "/logs/" + REC_LOG_NAME, shared); - enforceMaxLogSize(shared->tmpDir + "/logs/" + DET_LOG_NAME, shared); - - dumpLogs(shared->tmpDir + "/logs/" + REC_LOG_NAME, shared->recLog); - dumpLogs(shared->tmpDir + "/logs/" + DET_LOG_NAME, shared->detLog); - - shared->logMutex.lock(); - shared->recLog.clear(); - shared->detLog.clear(); - shared->logMutex.unlock(); - enforceMaxEvents(shared); enforceMaxImages(shared); enforceMaxVids(shared); @@ -235,7 +101,7 @@ bool EventLoop::exec() if (vidList.size() > 1) { - recLog("attempting write out of event: " + name, shared); + QTextStream(stdout) << "attempting write out of event: " << name << Qt::endl; if (wrOutVod()) { @@ -244,7 +110,7 @@ bool EventLoop::exec() args << "convert"; args << imgPath; - args << shared->outDir + "/" + name + shared->thumbExt; + args << shared->recPath + "/" + name + shared->thumbExt; proc.start("magick", args); proc.waitForFinished(); @@ -257,8 +123,6 @@ bool EventLoop::exec() vidList.clear(); } - shared->recMutex.lock(); - QList rmIndx; for (auto i = 0; i < shared->recList.size(); ++i) @@ -277,8 +141,8 @@ bool EventLoop::exec() auto maxSecs = shared->evMaxSecs / 2; // half the maxsecs value to get front-back half secs - auto backFiles = backwardFacingFiles(shared->tmpDir + "/live", shared->streamExt, event->timeStamp, maxSecs); - auto frontFiles = forwardFacingFiles(shared->tmpDir + "/live", shared->streamExt, event->timeStamp, maxSecs); + auto backFiles = backwardFacingFiles(shared->buffPath + "/live", shared->streamExt, event->timeStamp, maxSecs); + auto frontFiles = forwardFacingFiles(shared->buffPath + "/live", shared->streamExt, event->timeStamp, maxSecs); vidList.append(backFiles + frontFiles); rmIndx.append(i); @@ -293,9 +157,7 @@ bool EventLoop::exec() { shared->recList.removeAt(i); } - - shared->recMutex.unlock(); - + return Loop::exec(); } @@ -311,25 +173,28 @@ bool EventLoop::wrOutVod() for (auto &&vid : vidList) { - recLog("event_src: " + vid, shared); + QTextStream(stdout) << "event_src: " << vid << Qt::endl; if (QFile::exists(vid)) { file.write(QString("file '" + vid + "'\n").toUtf8()); cnt++; } + else + { + QTextStream(stdout) << "warning: the event hls clip does not exists." << Qt::endl; + } } file.close(); if (cnt == 0) { - recLog("err: none of the event hls clips exists, canceling write out.", shared); + QTextStream(stderr) << "err: none of the event hls clips exists, canceling write out." << Qt::endl; QFile::remove(concat); } else { - QProcess proc; QStringList args; args << "-f"; @@ -337,24 +202,9 @@ bool EventLoop::wrOutVod() args << "-safe" << "0"; args << "-i" << concat; args << "-c" << "copy"; - args << shared->outDir + "/" + name + shared->recExt; - - proc.setProgram("ffmpeg"); - proc.setArguments(args); - proc.start(); - - if (proc.waitForStarted()) - { - recLog("concat_cmd_start: ok", shared); - - proc.waitForFinished(); ret = true; - } - else - { - recLog("concat_cmd_start: fail", shared); - recLog("concat_cmd_stderr: " + QString(proc.readAllStandardError()), shared); - } - + args << shared->recPath + "/" + name + shared->recExt; + + QProcess::execute("ffmpeg", args); QFile::remove(concat); } @@ -363,6 +213,7 @@ bool EventLoop::wrOutVod() DetectLoop::DetectLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent) { + seed = 0; pcTimer = 0; heartBeat = 2; delayCycles = 12; // this will be used to delay the @@ -391,24 +242,24 @@ void DetectLoop::pcBreak() { if (!shared->postCmd.isEmpty()) { - detLog("---POST_BREAK---", shared); + QTextStream(stdout) << "---POST_BREAK---" << Qt::endl; if (mod) { - detLog("motion detected, skipping the post command.", shared); + QTextStream(stdout) << "motion detected, skipping the post command." << Qt::endl; } else { if (delayCycles == 0) delayCycles = 5; else delayCycles += 5; - detLog("no motion detected, running post command: " + shared->postCmd, shared); + QTextStream(stdout) << "no motion detected, running post command: " << shared->postCmd << Qt::endl; auto args = parseArgs(shared->postCmd.toUtf8(), -1); if (args.isEmpty()) { - detLog("err: did not parse an executable from the post command line.", shared); + QTextStream(stderr) << "err: did not parse an executable from the post command line." << Qt::endl; } else { @@ -453,38 +304,63 @@ QStringList DetectLoop::buildArgs(const QString &prev, const QString &next) return args; } +QStringList DetectLoop::buildSnapArgs(const QString &vidSrc, const QString &imgPath) +{ + QStringList ret; + + ret.append("-y"); + ret.append("-i"); + ret.append(vidSrc); + ret.append("-frames:v"); + ret.append("1"); + ret.append(imgPath); + + return ret; +} + bool DetectLoop::exec() { if (delayCycles > 0) { delayCycles -= 1; - detLog("delay: detection cycle skipped. cycles left to be skipped: " + QString::number(delayCycles), shared); + QTextStream(stdout) << "delay: detection cycle skipped. cycles left to be skipped: " << QString::number(delayCycles) << Qt::endl; } else { - auto curDT = QDateTime::currentDateTime(); - auto images = backwardFacingFiles("img", ".bmp", curDT, 6); + auto imgAPath = "img/" + QString::number(seed++) + ".bmp"; + auto imgBPath = "img/" + QString::number(seed++) + ".bmp"; + auto curDT = QDateTime::currentDateTime(); + auto clips = backwardFacingFiles("live", shared->streamExt, curDT, 16); - if (images.size() < 2) + if (clips.size() < 2) { - detLog("wrn: didn't pick up enough image files from the image stream. number of files: " + QString::number(images.size()), shared); - detLog(" will try again on the next loop.", shared); + QTextStream(stdout) << "warning: didn't pick up enough clips files from the video stream. number of files: " << QString::number(clips.size()) << Qt::endl; + QTextStream(stdout) << " will try again on the next loop." << Qt::endl; } else { - auto pos = images.size() - 1; - auto args = buildArgs(images[pos - 1], images[pos]); + auto snapArgsA = buildSnapArgs(clips[clips.size() - 2], imgAPath); + auto snapArgsB = buildSnapArgs(clips[clips.size() - 1], imgAPath); + auto compArgs = buildArgs(imgAPath, imgBPath); - if (args.isEmpty()) + if (compArgs.isEmpty()) { - detLog("err: could not parse a executable name from img_comp_cmd: " + shared->compCmd, shared); + QTextStream(stderr) << "err: could not parse a executable name from img_comp_cmd: " << shared->compCmd << Qt::endl; } else { + QProcess snapFromVidA; + QProcess snapFromVidB; QProcess extComp; - extComp.start(args[0], args.mid(1)); + snapFromVidA.start("ffmpeg", snapArgsA); + snapFromVidA.waitForFinished(); + + snapFromVidB.start("ffmpeg", snapArgsB); + snapFromVidB.waitForFinished(); + + extComp.start(compArgs[0], compArgs.mid(1)); extComp.waitForFinished(); float score = 0; @@ -505,26 +381,26 @@ bool DetectLoop::exec() if (!ok) { - detLog("err: img_comp_out: " + shared->outputType + " is not valid. it must be 'stdout' or 'stderr'" , shared); + QTextStream(stderr) << "err: img_comp_out: " << shared->outputType << " is not valid. it must be 'stdout' or 'stderr'" << Qt::endl; } else { - detLog(args.join(" ") + " --result: " + QString::number(score), shared); + QTextStream(stdout) << compArgs.join(" ") << " --result: " << QString::number(score) << Qt::endl; if (score >= shared->imgThresh) { - detLog("--threshold_breached: " + QString::number(shared->imgThresh), shared); + QTextStream(stdout) << "--threshold_breached: " << QString::number(shared->imgThresh) << Qt::endl; evt_t event; event.timeStamp = curDT; event.score = score; - event.imgPath = images[pos]; + event.imgPath = imgBPath; event.queAge = 0; - - shared->recMutex.lock(); - shared->recList.append(event); mod = true; - shared->recMutex.unlock(); + + mod = true; + + shared->recList.append(event); } } } diff --git a/src/camera.h b/src/camera.h index 883d42e..3897cbf 100644 --- a/src/camera.h +++ b/src/camera.h @@ -14,7 +14,6 @@ // GNU General Public License for more details. #include "common.h" -#include "logger.h" class Camera : public QObject { @@ -56,32 +55,6 @@ public: virtual bool exec(); }; -class RecLoop : public Loop -{ - Q_OBJECT - -private: - - QProcess *recProc; - QProcess *imgProc; - QString curUrl; - - void updateCmd(); - void procError(const QString &desc, QProcess *proc); - -private slots: - - void init(); - void term(); - void rdProcErr(); - -public: - - explicit RecLoop(shared_t *shared, QThread *thr, QObject *parent = nullptr); - - bool exec(); -}; - class Upkeep : public Loop { Q_OBJECT @@ -123,10 +96,12 @@ private: QTimer *pcTimer; uint delayCycles; bool mod; + quint64 seed; void resetTimers(); float getFloatFromExe(const QByteArray &line); QStringList buildArgs(const QString &prev, const QString &next); + QStringList buildSnapArgs(const QString &vidSrc, const QString &imgPath); private slots: diff --git a/src/common.cpp b/src/common.cpp index 8e27bfd..73bc1ce 100644 --- a/src/common.cpp +++ b/src/common.cpp @@ -107,11 +107,11 @@ QStringList forwardFacingFiles(const QString &path, const QString &ext, const QD void enforceMaxEvents(shared_t *share) { - auto names = lsFilesInDir(share->outDir, share->recExt); + auto names = lsFilesInDir(share->recPath, share->recExt); while (names.size() > share->maxEvents) { - auto nameOnly = share->outDir + "/" + names[0]; + auto nameOnly = share->recPath + "/" + names[0]; nameOnly.chop(share->recExt.size()); @@ -127,11 +127,11 @@ void enforceMaxEvents(shared_t *share) void enforceMaxImages(shared_t *share) { - auto names = lsFilesInDir(share->tmpDir + "/img", ".bmp"); + auto names = lsFilesInDir(share->buffPath + "/img", ".bmp"); while (names.size() > MAX_IMAGES) { - QFile::remove(share->tmpDir + "/img/" + names[0]); + QFile::remove(share->buffPath + "/img/" + names[0]); names.removeFirst(); } @@ -139,11 +139,11 @@ void enforceMaxImages(shared_t *share) void enforceMaxVids(shared_t *share) { - auto names = lsFilesInDir(share->tmpDir + "/live", share->streamExt); + auto names = lsFilesInDir(share->buffPath + "/live", share->streamExt); while (names.size() > MAX_VIDEOS) { - QFile::remove(share->tmpDir + "/live/" + names[0]); + QFile::remove(share->buffPath + "/live/" + names[0]); names.removeFirst(); } @@ -153,7 +153,7 @@ void rdLine(const QString ¶m, const QString &line, QString *value) { if (line.startsWith(param)) { - *value = line.mid(param.size()); + *value = line.mid(param.size()).trimmed(); } } @@ -161,7 +161,7 @@ void rdLine(const QString ¶m, const QString &line, int *value) { if (line.startsWith(param)) { - *value = line.mid(param.size()).toInt(); + *value = line.mid(param.size()).trimmed().toInt(); } } @@ -210,6 +210,8 @@ bool rdConf(const QString &filePath, shared_t *share) share->recordUri.clear(); share->postCmd.clear(); share->camName.clear(); + share->buffPath.clear(); + share->recPath.clear(); auto thrCount = QThread::idealThreadCount() / 2; @@ -221,20 +223,18 @@ bool rdConf(const QString &filePath, shared_t *share) share->postSecs = 60; share->evMaxSecs = 30; share->conf = filePath; - share->buffPath = "/var/buffer"; - share->recPath = "/var/footage"; share->outputType = "stderr"; share->compCmd = "magick compare -metric FUZZ " + QString(PREV_IMG) + " " + QString(NEXT_IMG) + " /dev/null"; share->streamCodec = "copy"; share->streamExt = ".ts"; share->recExt = ".mp4"; share->thumbExt = ".jpg"; - share->singleTenant = false; share->imgThreads = thrCount; share->recThreads = thrCount; share->recFps = 30; share->recScale = "1280:720"; share->imgScale = "320:240"; + share->servUser = APP_BIN; QString line; @@ -260,12 +260,12 @@ bool rdConf(const QString &filePath, shared_t *share) rdLine("stream_ext = ", line, &share->streamExt); rdLine("rec_ext = ", line, &share->recExt); rdLine("thumbnail_ext = ", line, &share->thumbExt); - rdLine("single_tenant = ", line, &share->singleTenant); rdLine("img_threads = ", line, &share->imgThreads); rdLine("rec_threads = ", line, &share->recThreads); rdLine("rec_fps = ", line, &share->recFps); rdLine("rec_scale = ", line, &share->recScale); rdLine("img_scale = ", line, &share->imgScale); + rdLine("service_user = ", line, &share->servUser); } } while(!line.isEmpty()); @@ -279,178 +279,53 @@ bool rdConf(const QString &filePath, shared_t *share) extCorrection(share->recExt); extCorrection(share->thumbExt); - if (share->singleTenant) + if (share->buffPath.isEmpty()) { - share->outDir = QDir().cleanPath(share->recPath); - share->tmpDir = QDir().cleanPath(share->buffPath); + share->buffPath = "/var/buffer/" + share->camName; } else { - share->outDir = QDir().cleanPath(share->recPath) + "/" + share->camName; - share->tmpDir = QDir().cleanPath(share->buffPath) + "/" + share->camName; + share->buffPath = QDir::cleanPath(share->buffPath); } - auto servDir = QString("/var/") + APP_BIN + QString("_serv"); + if (share->recPath.isEmpty()) + { + share->recPath = "/var/footage/" + share->camName; + } + else + { + share->recPath = QDir::cleanPath(share->recPath); + } share->retCode = EACCES; - if (!mkPath(servDir)) - { - QTextStream(stderr) << "err: failed to create service directory - " << servDir << " check for write permissions." << Qt::endl; - } - else if (!mkPath(share->recPath)) + if (!mkPath(share->recPath)) { - QTextStream(stderr) << "err: failed to create root recording directory - " << share->recPath << " check for write permissions." << Qt::endl; + QTextStream(stderr) << "err: failed to create recording directory - " << share->recPath << " check for write permissions." << Qt::endl; } else if (!mkPath(share->buffPath)) { - QTextStream(stderr) << "err: failed to create root buffer directory - " << share->buffPath << " check for write permissions." << Qt::endl; + QTextStream(stderr) << "err: failed to create buffer directory - " << share->buffPath << " check for write permissions." << Qt::endl; } - else if (!mkPath(share->outDir)) + else if (!mkPath(share->buffPath + "/img")) { - QTextStream(stderr) << "err: failed to create recording directory - " << share->outDir << " check for write permissions." << Qt::endl; + QTextStream(stderr) << "err: failed to create 'img' in the buffer directory - " << share->buffPath << "/img" << " check for write permissions." << Qt::endl; } - else if (!mkPath(share->tmpDir)) + else if (!mkPath(share->buffPath + "/live")) { - QTextStream(stderr) << "err: failed to create buffer directory - " << share->tmpDir << " check for write permissions." << Qt::endl; - } - else if (!mkPath(share->tmpDir + "/live")) - { - QTextStream(stderr) << "err: failed to create 'live' in the buffer directory - " << share->tmpDir << "/live" << " check for write permissions." << Qt::endl; - } - else if (!mkPath(share->tmpDir + "/logs")) - { - QTextStream(stderr) << "err: failed to create 'logs' in the buffer directory - " << share->tmpDir << "/logs" << " check for write permissions." << Qt::endl; - } - else if (!mkPath(share->tmpDir + "/img")) - { - QTextStream(stderr) << "err: failed to create 'img' in the buffer directory - " << share->tmpDir << "/img" << " check for write permissions." << Qt::endl; + QTextStream(stderr) << "err: failed to create 'live' in the buffer directory - " << share->buffPath << "/live" << " check for write permissions." << Qt::endl; } else { - share->retCode = 0; - share->servPath = QString("/var/") + APP_BIN + QString("_serv/") + APP_BIN + "." + share->camName + ".service"; + share->retCode = 0; + share->servMainLoop = QString(APP_BIN) + ".main_loop." + share->camName; + share->servVidLoop = QString(APP_BIN) + ".vid_loop." + share->camName; } } return share->retCode == 0; } -void rmServices() -{ - auto path = QString("/var/") + APP_BIN + QString("_serv"); - auto files = lsFilesInDir(path, ".service"); - - for (auto &&serv : files) - { - QProcess::execute("systemctl", {"stop", serv}); - QProcess::execute("systemctl", {"disable", serv}); - - QFile::remove(QString("/lib/systemd/system/") + serv); - QFile::remove(path + "/" + serv); - } - - QProcess::execute("systemctl", {"daemon-reload"}); -} - -void listServices() -{ - auto path = QString("/var/") + APP_BIN + QString("_serv"); - auto files = lsFilesInDir(path, ".service"); - - for (auto &&serv : files) - { - QTextStream(stdout) << serv << ": "; QProcess::execute("systemctl", {"is-active", serv}); - } -} - -int loadServices(const QStringList &args) -{ - auto ret = ENOENT; - auto path = QDir().cleanPath(getParam("-d", args)); - auto files = lsFilesInDir(path); - - if (!QDir(path).exists()) - { - QTextStream(stderr) << "err: the supplied directory in -d '" << path << "' does not exists or is not a directory." << Qt::endl; - } - else if (files.isEmpty()) - { - QTextStream(stderr) << "err: no config files found in '" << path << "'" << Qt::endl; - } - else - { - ret = 0; - - QTextStream(stdout) << "loading conf files from dir: " << path << Qt::endl; - - for (auto &&conf : files) - { - shared_t shared; - - if (!rdConf(path + "/" + conf, &shared)) - { - ret = shared.retCode; break; - } - else - { - QTextStream(stdout) << conf << " --" << Qt::endl; - - QFile file(shared.servPath); - - if (!file.open(QFile::ReadWrite | QFile::Truncate)) - { - QTextStream(stderr) << "err: failed to open service file: " << shared.servPath << " for writing. reason: " << file.errorString(); - - ret = EACCES; file.close(); break; - } - else - { - file.write("[Unit]\n"); - file.write("Description=" + QByteArray(APP_NAME) + " Camera - " + shared.camName.toUtf8() + "\n"); - file.write("After=network.target\n\n"); - file.write("[Service]\n"); - file.write("Type=simple\n"); - file.write("User=" + QByteArray(APP_BIN) + "\n"); - file.write("Restart=always\n"); - file.write("RestartSec=5\n"); - file.write("TimeoutStopSec=infinity\n"); - file.write("ExecStart=/usr/bin/env " + QByteArray(APP_BIN) + " -c " + shared.conf.toUtf8() + "\n\n"); - file.write("[Install]\n"); - file.write("WantedBy=multi-user.target"); - file.close(); - - auto servName = QFileInfo(shared.servPath).fileName(); - - if (!QFile::link(shared.servPath, "/lib/systemd/system/" + servName)) - { - ret = EACCES; break; - } - else - { - if (ret == 0) ret = QProcess::execute("systemctl", {"daemon-reload"}); - if (ret == 0) ret = QProcess::execute("systemctl", {"enable", servName}); - if (ret == 0) ret = QProcess::execute("systemctl", {"start", servName}); - - if (ret != 0) - { - break; - } - else - { - QTextStream(stdout) << "Successfully loaded camera service: " << servName << Qt::endl; - - if (shared.singleTenant) break; - } - } - } - } - } - } - - return ret; -} - QString buildThreadCount(int count) { QString ret = "0"; diff --git a/src/common.h b/src/common.h index 259dd0b..7529119 100644 --- a/src/common.h +++ b/src/common.h @@ -29,7 +29,7 @@ using namespace std; -#define APP_VER "3.3.t2" +#define APP_VER "3.3.t3" #define APP_NAME "Motion Watch" #define APP_BIN "mow" #define REC_LOG_NAME "rec.log" @@ -42,6 +42,13 @@ using namespace std; #define PREV_IMG "&prev&" #define NEXT_IMG "&next&" +enum CmdExeType +{ + MAIN_LOOP, + VID_LOOP, + IMG_LOOP +}; + struct evt_t { QDateTime timeStamp; @@ -59,13 +66,10 @@ struct shared_t QString recLog; QString detLog; QString recordUri; - QString outDir; - QString tmpDir; QString buffPath; QString postCmd; QString camName; QString recPath; - QString servPath; QString outputType; QString compCmd; QString streamCodec; @@ -74,6 +78,10 @@ struct shared_t QString thumbExt; QString recScale; QString imgScale; + QString servMainLoop; + QString servVidLoop; + QString servImgLoop; + QString servUser; bool singleTenant; bool skipCmd; int recFps; @@ -97,9 +105,6 @@ QStringList forwardFacingFiles(const QString &path, const QString &ext, const QD QStringList parseArgs(const QByteArray &data, int maxArgs, int *pos = nullptr); bool rdConf(const QString &filePath, shared_t *share); bool mkPath(const QString &path); -int loadServices(const QStringList &args); -void listServices(); -void rmServices(); void rdLine(const QString ¶m, const QString &line, QString *value); void rdLine(const QString ¶m, const QString &line, int *value); void rdLine(const QString ¶m, const QString &line, bool *value); diff --git a/src/logger.cpp b/src/logger.cpp deleted file mode 100644 index 8263a2a..0000000 --- a/src/logger.cpp +++ /dev/null @@ -1,68 +0,0 @@ -// This file is part of Motion Watch. - -// Motion Watch 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. - -// Motion Watch 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. - -#include "logger.h" - -void wrLogLine(const QString &line, shared_t *share, QString &body) -{ - if (!line.isEmpty()) - { - share->logMutex.lock(); - - body += QDateTime::currentDateTime().toString("[yyyy-MM-dd-hh-mm-ss] ") + line + "\n"; - - share->logMutex.unlock(); - } -} - -void recLog(const QString &line, shared_t *share) -{ - wrLogLine(line, share, share->recLog); -} - -void detLog(const QString &line, shared_t *share) -{ - wrLogLine(line, share, share->detLog); -} - -void enforceMaxLogSize(const QString &filePath, shared_t *share) -{ - QFile file(filePath); - - if (file.exists()) - { - if (file.size() >= share->maxLogSize) - { - file.remove(); - } - } -} - -void dumpLogs(const QString &fileName, const QString &lines) -{ - if (!lines.isEmpty()) - { - QFile outFile(fileName); - - if (outFile.exists()) - { - outFile.open(QFile::Append); - } - else - { - outFile.open(QFile::WriteOnly); - } - - outFile.write(lines.toUtf8()); - outFile.close(); - } -} diff --git a/src/logger.h b/src/logger.h deleted file mode 100644 index 00a1685..0000000 --- a/src/logger.h +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef lOGGER_H -#define lOGGER_H - -// This file is part of Motion Watch. - -// Motion Watch 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. - -// Motion Watch 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. - -#include "common.h" - -void wrLogLine(const QString &line, shared_t *share, QString &body); -void recLog(const QString &line, shared_t *share); -void detLog(const QString &line, shared_t *share); -void dumpLogs(const QString &fileName, const QString &lines); -void enforceMaxLogSize(const QString &filePath, shared_t *share); - -#endif // lOGGER_H diff --git a/src/main.cpp b/src/main.cpp index 4fe5461..66b3e5a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -12,26 +12,23 @@ #include "common.h" #include "camera.h" +#include "services.h" void showHelp() { QTextStream(stdout) << APP_NAME << " " << APP_VER << Qt::endl << Qt::endl; QTextStream(stdout) << "Usage: mow " << Qt::endl << Qt::endl; QTextStream(stdout) << "-h : display usage information about this application." << Qt::endl; - QTextStream(stdout) << "-c : path to the config file used to run a single camera instance." << Qt::endl; - QTextStream(stdout) << "-d : path to a directory that can contain multiple config files." << Qt::endl; - QTextStream(stdout) << " each file found in the directory will be used to create a " << Qt::endl; - QTextStream(stdout) << " systemd service for the camera. already existing camera" << Qt::endl; - QTextStream(stdout) << " services will be reloaded and restarted. any services without" << Qt::endl; - QTextStream(stdout) << " a config file will be removed." << Qt::endl; - QTextStream(stdout) << "-i : this is the same as -d except a directory does not need to be" << Qt::endl; - QTextStream(stdout) << " provided. the default directory /etc/" << APP_BIN << " will be used." << Qt::endl; + QTextStream(stdout) << "-c : path to the config file used to run a single main loop instance." << Qt::endl; + QTextStream(stdout) << "-i : all valid config files found in /etc/mow will be used to create" << Qt::endl; + QTextStream(stdout) << " a full instance; full meaning main and vid loop systemd services" << Qt::endl; + QTextStream(stdout) << " will be created for each config file." << Qt::endl; QTextStream(stdout) << "-v : display the current version." << Qt::endl; QTextStream(stdout) << "-u : uninstall the entire app from your system, including all" << Qt::endl; - QTextStream(stdout) << " camera services." << Qt::endl; + QTextStream(stdout) << " systemd services related to it." << Qt::endl; QTextStream(stdout) << "-f : force an action without pausing for user confirmation." << Qt::endl; - QTextStream(stdout) << "-l : list all camera services along with statuses." << Qt::endl; - QTextStream(stdout) << "-r : remove all camera services." << Qt::endl; + QTextStream(stdout) << "-l : list all mow services along with statuses." << Qt::endl; + QTextStream(stdout) << "-r : remove all mow services." << Qt::endl; } int main(int argc, char** argv) @@ -52,21 +49,22 @@ int main(int argc, char** argv) { QTextStream(stdout) << APP_VER << Qt::endl; } - else if (args.contains("-d")) - { - rmServices(); ret = loadServices(args); - } else if (args.contains("-l")) { - listServices(); + servStatByDir("/etc/mow"); } - else if (args.contains("-i")) - { - args.clear(); - args.append("-d"); - args.append("/etc/" + QString(APP_BIN)); - - rmServices(); ret = loadServices(args); + else if (args.contains("-i") || args.contains("-d")) + { + ret = rmServiceByDir("/etc/mow"); + + if ((ret == 0) && args.contains("-i")) + { + ret = loadServiceByDir("/etc/mow", true); + } + else if (ret == 0) + { + ret = loadServiceByDir("/etc/mow", false); + } } else if (args.contains("-c")) { @@ -83,7 +81,12 @@ int main(int argc, char** argv) { if (args.contains("-f")) { - rmServices(); QProcess::execute("/opt/mow/uninst", QStringList()); + ret = rmServiceByDir("/etc/mow"); + + if (ret == 0) + { + ret = QProcess::execute("/opt/mow/uninst", QStringList()); + } } else { @@ -94,13 +97,18 @@ int main(int argc, char** argv) if (ans == 'y' || ans == 'Y') { - rmServices(); QProcess::execute("/opt/mow/uninst", QStringList()); + ret = rmServiceByDir("/etc/mow"); + + if (ret == 0) + { + ret = QProcess::execute("/opt/mow/uninst", QStringList()); + } } } } else if (args.contains("-r")) { - rmServices(); + ret = rmServiceByDir("/etc/mow"); } else { diff --git a/src/services.cpp b/src/services.cpp new file mode 100644 index 0000000..55adaf0 --- /dev/null +++ b/src/services.cpp @@ -0,0 +1,243 @@ +// This file is part of Motion Watch. + +// Motion Watch 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. + +// Motion Watch 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. + +#include "services.h" + +int loadService(const QString &desc, const QString &user, const QString &servName, const QString &workDir, const QString &recPath, bool start) +{ + QFile file("/lib/systemd/system/" + servName + ".service"); + + auto ret = 0; + auto exists = file.exists(); + + if (!file.open(QFile::ReadWrite | QFile::Truncate)) + { + QTextStream(stderr) << "err: failed to open service file: " << file.fileName() << " for writing. reason: " << file.errorString() << Qt::endl; + + ret = EACCES; + } + else + { + if (exists) + { + if (ret == 0) ret = QProcess::execute("systemctl", {"stop", servName}); + if (ret == 0) ret = QProcess::execute("systemctl", {"disable", servName}); + } + + if (ret == 0) + { + file.write("[Unit]\n"); + file.write("Description=" + desc.toUtf8() + "\n"); + file.write("After=network.target\n\n"); + file.write("[Service]\n"); + file.write("WorkingDirectory=" + workDir.toUtf8() + "\n"); + file.write("Type=simple\n"); + file.write("User=" + user.toUtf8() + "\n"); + file.write("Restart=always\n"); + file.write("TimeoutStopSec=infinity\n"); + file.write("ExecStart=/usr/bin/env " + servName.toUtf8() + "\n\n"); + file.write("[Install]\n"); + file.write("WantedBy=multi-user.target\n"); + file.close(); + + if (ret == 0) ret = QProcess::execute("chown", {"-R", user + ":" + user, workDir}); + if (ret == 0) ret = QProcess::execute("chown", {"-R", user + ":" + user, recPath}); + if (ret == 0) ret = QProcess::execute("systemctl", {"daemon-reload"}); + + if (start) + { + if (ret == 0) ret = QProcess::execute("systemctl", {"enable", servName}); + if (ret == 0) ret = QProcess::execute("systemctl", {"start", servName}); + } + } + } + + file.close(); + + return ret; +} + +int loadSh(const QString &name, const QString &exeCmd, const QString &workDir) +{ + QFile file("/usr/bin/" + name); + + auto ret = 0; + + if (!file.open(QFile::ReadWrite | QFile::Truncate)) + { + QTextStream(stderr) << "err: failed to open shell script file: " << file.fileName() << " for writing. reason: " << file.errorString() << Qt::endl; + + ret = EACCES; + } + else + { + file.write("#!/bin/sh\n\n"); + file.write("cd " + workDir.toUtf8() + "\n"); + file.write(exeCmd.toUtf8() + "\n"); + file.close(); + + QProcess::execute("chmod", {"-v", "+x", file.fileName()}); + } + + file.close(); + + return ret; +} + +QString camCmdFromConf(shared_t *conf, CmdExeType type) +{ + QString ret = ""; + + if (type == MAIN_LOOP) + { + ret += QString(APP_BIN) + " -c " + conf->conf; + } + else + { + ret += "taskset -c " + buildThreadCount(conf->recThreads) + " "; + ret += "ffmpeg -hide_banner -i '" + conf->recordUri + "' -strftime 1 -strftime_mkdir 1 "; + + if (conf->recordUri.contains("rtsp")) + { + ret += "-rtsp_transport tcp "; + } + + ret += "-vf fps=" + QString::number(conf->recFps) + ",scale=" + conf->recScale + " "; + ret += "-hls_segment_filename live/" + QString(STRFTIME_FMT) + conf->streamExt + " "; + ret += "-y -vcodec " + conf->streamCodec + " -f hls -hls_time 2 -hls_list_size 1000 -hls_flags append_list+omit_endlist -t 60 stream.m3u8"; + } + + return ret; +} + +int loadServiceByConf(const QString &confFile, bool start) +{ + auto ret = 0; + + shared_t conf; + + if (!rdConf(confFile, &conf)) + { + ret = conf.retCode; + } + else + { + auto desc = QString(APP_NAME) + " - " + conf.camName; + + if (ret == 0) ret = loadSh(conf.servMainLoop, camCmdFromConf(&conf, MAIN_LOOP), conf.buffPath); + if (ret == 0) ret = loadSh(conf.servVidLoop, camCmdFromConf(&conf, VID_LOOP), conf.buffPath); + + if (ret == 0) ret = loadService(desc, conf.servUser, conf.servMainLoop, conf.buffPath, conf.recPath, start); + if (ret == 0) ret = loadService(desc, conf.servUser, conf.servVidLoop, conf.buffPath, conf.recPath, start); + } + + return ret; +} + +int rmService(const QString &servName) +{ + auto path = "/lib/systemd/system/" + servName + ".service"; + auto ret = 0; + + if (QFile::exists(path)) + { + ret = QProcess::execute("systemctl", {"stop", servName}); + ret = QProcess::execute("systemctl", {"disable", servName}); + + QFile::remove(path); + QFile::remove("/usr/bin/" + servName); + } + + return ret; +} + +int rmServiceByConf(const QString &confFile) +{ + shared_t conf; + + if (rdConf(confFile, &conf)) + { + conf.retCode = rmService(conf.servMainLoop); + conf.retCode = rmService(conf.servVidLoop); + } + + return conf.retCode; +} + +void servStatByConf(const QString &confFile) +{ + shared_t conf; + + if (rdConf(confFile, &conf)) + { + QTextStream cout(stdout); + + cout << "--" << conf.camName << Qt::endl; + cout << " " << conf.servMainLoop << ": "; + + if (QFile::exists("/lib/systemd/system/" + conf.servMainLoop + ".service")) + { + QProcess::execute("systemctl", {"is-enabled", conf.servMainLoop}); cout << Qt::endl; + } + else + { + cout << "Not Installed" << Qt::endl; + } + + cout << " " << conf.servVidLoop << ": "; + + if (QFile::exists("/lib/systemd/system/" + conf.servVidLoop + ".service")) + { + QProcess::execute("systemctl", {"is-enabled", conf.servVidLoop}); cout << Qt::endl; + } + else + { + cout << "Not Installed" << Qt::endl; + } + } +} + +void servStatByDir(const QString &path) +{ + auto files = lsFilesInDir(path); + + for (auto &&conf : files) + { + servStatByConf(QDir::cleanPath(path) + "/" + conf); + } +} + +int loadServiceByDir(const QString &path, bool start) +{ + auto ret = 0; + auto files = lsFilesInDir(path); + + for (auto &&conf : files) + { + if (ret == 0 ) ret = loadServiceByConf(QDir::cleanPath(path) + "/" + conf, start); + } + + return ret; +} + +int rmServiceByDir(const QString &path) +{ + auto ret = 0; + auto files = lsFilesInDir(path); + + for (auto &&conf : files) + { + if (ret == 0 ) ret = rmServiceByConf(QDir::cleanPath(path) + "/" + conf); + } + + return ret; +} diff --git a/src/services.h b/src/services.h new file mode 100644 index 0000000..576927b --- /dev/null +++ b/src/services.h @@ -0,0 +1,29 @@ +#ifndef SERVICES_H +#define SERVICES_H + +// This file is part of Motion Watch. + +// Motion Watch 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. + +// Motion Watch 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. + +#include "common.h" + +QString camCmdFromConf(shared_t *conf, CmdExeType type); +void servStatByDir(const QString &path); +void servStatByConf(const QString &confFile); +int loadService(const QString &desc, const QString &user, const QString &servName, const QString &workDir, const QString &recPath, bool start); +int loadServiceByConf(const QString &confFile, bool start); +int loadServiceByDir(const QString &path, bool start); +int rmServiceByConf(const QString &confFile); +int rmService(const QString &servName); +int rmServiceByDir(const QString &path); +int loadSh(const QString &name, const QString &exeCmd, const QString &workDir); + +#endif // SERVICES_H