-moved all service related code to seperate files.
-split the camera service into 2 systemd services.
-the -i, -c and -d options now function differently to support the
 split systemd services.
-internal logging was completely removed. all verbose output will
 instead just go to stderr/stdout. will use journalctl for real
 time logging instead.
-fixed the magick compile/install script so it will actually run
 now.
-fixed a bug in the config file reading functions so it will now
 trim off white spaces and line breaks at the ends. doing this
 now prevents undefined behaviour if any parameter has a line
 break in it.
-service user was added as a config file option. this can be used
 to set user name the installed services will run under. the
 default user is "mow."
-the install.sh script will now add the default mow user to the
 video group making it possible to record footage from webcams
 without permission issues.
This commit is contained in:
Zii 2023-10-27 15:43:17 -04:00
parent 83080cfe41
commit 525c342c0f
12 changed files with 427 additions and 513 deletions

View File

@ -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})

View File

@ -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

View File

@ -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

View File

@ -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<int> 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);
@ -294,8 +158,6 @@ 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);
}
}
}

View File

@ -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:

View File

@ -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 &param, 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 &param, 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))
if (!mkPath(share->recPath))
{
QTextStream(stderr) << "err: failed to create service directory - " << servDir << " check for write permissions." << Qt::endl;
}
else 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";

View File

@ -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 &param, const QString &line, QString *value);
void rdLine(const QString &param, const QString &line, int *value);
void rdLine(const QString &param, const QString &line, bool *value);

View File

@ -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();
}
}

View File

@ -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

View File

@ -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 <argument>" << 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"))
else if (args.contains("-i") || args.contains("-d"))
{
args.clear();
args.append("-d");
args.append("/etc/" + QString(APP_BIN));
ret = rmServiceByDir("/etc/mow");
rmServices(); ret = loadServices(args);
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
{

243
src/services.cpp Normal file
View File

@ -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;
}

29
src/services.h Normal file
View File

@ -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