diff --git a/mythtv/libs/libmythbase/housekeeper.cpp b/mythtv/libs/libmythbase/housekeeper.cpp new file mode 100644 index 00000000000..8ced5b1a300 --- /dev/null +++ b/mythtv/libs/libmythbase/housekeeper.cpp @@ -0,0 +1,629 @@ +/* -*- Mode: c++ -*- +* +* Class HouseKeeperTask +* Class HouseKeeperThread +* Class HouseKeeper +* +* Copyright (C) Raymond Wagner 2013 +* +* 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 2 of the License, or +* (at your option) any later version. +* +* 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, write to the Free Software +* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +*/ + + + +/** \defgroup housekeeper HouseKeeper + * \ingroup libmythbase + * \brief Framework for handling frequently run background tasks + * + * This utility provides the ability to call maintenance tasks in the + * background at regular intervals. The basic operation consists of a + * manager class to store the definitions for the various tasks, the + * task definitions themselves, and one or more threads to run them in. + * + * The manager does not get its own thread, but rather is called at one + * minute granularity by a timer and the main application thread. This + * necessitates that the tasks performed by the manager be kept to a + * minimum. The manager loops through all task definitions, and checks + * to see if any are ready to be run. If any are ready, they are copied + * to the queue, and the run thread is woken up. + * + * The run thread sequentially steps through all tasks in the queue and + * then goes back to sleep. If one run thread is still active by the time + * the timer triggers a new pass, the existing thread will be discarded, + * and a new thread will be started to process remaining tasks in the queue. + * The old thread will terminate itself once finished with its current task, + * rather than following through with the next item in the queue, or going + * into wait. This allows the manager to deal with both time-sensitive + * tasks and long duration tasks, without any special knowledge of behavior. + */ + + +#include + +#include "mythevent.h" +#include "mythdbcon.h" +#include "housekeeper.h" +#include "mythcorecontext.h" + +HouseKeeperTask::HouseKeeperTask(const QString &dbTag, HouseKeeperScope scope, + HouseKeeperStartup startup): + ReferenceCounter(dbTag), m_dbTag(dbTag), m_confirm(false), m_scope(scope), + m_startup(startup), m_lastRun(MythDate::fromTime_t(0)) +{ +} + +bool HouseKeeperTask::CheckRun(QDateTime now) +{ + bool check = false; + if (!m_confirm && (check = DoCheckRun(now))) + // if m_confirm is already set, the task is already in the queue + // and should not be queued a second time + m_confirm = true; + return check; +} + +bool HouseKeeperTask::CheckImmediate(void) +{ + return (m_startup == kHKRunImmediateOnStartup); +} + +bool HouseKeeperTask::CheckStartup(void) +{ + if (m_startup == kHKRunOnStartup) + { + m_confirm = true; + return true; + } + return false; +} + +bool HouseKeeperTask::Run(void) +{ + LOG(VB_GENERAL, LOG_INFO, QString("Running HouseKeeperTask '%1'.") + .arg(m_dbTag)); + return DoRun(); +} + +QDateTime HouseKeeperTask::QueryLastRun(void) +{ + if (m_scope != kHKInst) + { + MSqlQuery query(MSqlQuery::InitCon()); + + m_lastRun = MythDate::fromTime_t(0); + + if (m_scope == kHKGlobal) + { + query.prepare("SELECT lastrun FROM housekeeping" + " WHERE tag = :TAG" + " AND hostname IS NULL"); + } + else + { + query.prepare("SELECT lastrun FROM housekeeping" + " WHERE tag = :TAG" + " AND hostname = :HOST"); + query.bindValue(":HOST", gCoreContext->GetHostName()); + } + + query.bindValue(":TAG", m_dbTag); + + if (query.exec() && query.next()) + { + m_lastRun = MythDate::as_utc(query.value(0).toDateTime()); + } + } + + return m_lastRun; +} + +QDateTime HouseKeeperTask::UpdateLastRun(QDateTime last) +{ + if (m_scope != kHKInst) + { + MSqlQuery query(MSqlQuery::InitCon()); + if (!query.isConnected()) + return last; + + if (m_lastRun == MythDate::fromTime_t(0)) + { + // not previously set, perform insert + + if (m_scope == kHKGlobal) + query.prepare("INSERT INTO housekeeping (tag, lastrun)" + " VALUES (:TAG, :TIME)"); + else + query.prepare("INSERT INTO housekeeping" + " ( tag, hostname, lastrun)" + " VALUES (:TAG, :HOST, :TIME)"); + } + else + { + // previously set, perform update + + if (m_scope == kHKGlobal) + query.prepare("UPDATE housekeeping SET lastrun=:TIME" + " WHERE tag = :TAG" + " AND hostname IS NULL"); + else + query.prepare("UPDATE housekeeping SET lastrun=:TIME" + " WHERE tag = :TAG" + " AND hostname = :HOST"); + } + + if (m_scope == kHKGlobal) + query.bindValue(":HOST", gCoreContext->GetHostName()); + query.bindValue(":TAG", m_dbTag); + query.bindValue(":TIME", last); + + if (!query.exec()) + MythDB::DBError("HouseKeeperTask::updateLastRun", query); + } + + m_lastRun = last; + m_confirm = false; + + QString msg = QString("HOUSE_KEEPER_RUNNING %1 %2 %3") + .arg(gCoreContext->GetHostName()).arg(m_dbTag) + .arg(MythDate::toString(last, MythDate::ISODate)); + gCoreContext->SendEvent(MythEvent(msg)); + + return last; +} + +PeriodicHouseKeeperTask::PeriodicHouseKeeperTask(const QString &dbTag, + int period, float min, float max, HouseKeeperScope scope, + HouseKeeperStartup startup) : + HouseKeeperTask(dbTag, scope, startup), m_period(period), + m_windowPercent(min, max), m_currentProb(1.0) +{ + CalculateWindow(); +} + +void PeriodicHouseKeeperTask::CalculateWindow(void) +{ + m_windowElapsed.first = + (uint32_t)((float)m_period * m_windowPercent.first); + m_windowElapsed.second = + (uint32_t)((float)m_period * m_windowPercent.second); +} + +void PeriodicHouseKeeperTask::SetWindow(float min, float max) +{ + m_windowPercent.first = min; + m_windowPercent.second = max; + CalculateWindow(); +} + +QDateTime PeriodicHouseKeeperTask::UpdateLastRun(QDateTime last) +{ + QDateTime res = HouseKeeperTask::UpdateLastRun(last); + CalculateWindow(); + m_currentProb = 1.0; + return res; +} + +void PeriodicHouseKeeperTask::SetLastRun(QDateTime last) +{ + HouseKeeperTask::SetLastRun(last); + CalculateWindow(); + m_currentProb = 1.0; +} + +bool PeriodicHouseKeeperTask::DoCheckRun(QDateTime now) +{ + int elapsed = GetLastRun().secsTo(now); + + if (elapsed < 0) + // something bad has happened. let's just move along + return false; + + if (elapsed < m_windowElapsed.first) + // insufficient time elapsed to test + return false; + if (elapsed > m_windowElapsed.second) + // too much time has passed. force run + return true; + + // calculate probability that task should not have yet run + // it's backwards, but it makes the math simplier + float prob = 1.0 - ((float)(elapsed - m_windowElapsed.first) / + (float)(m_windowElapsed.second - m_windowElapsed.first)); + if (m_currentProb < prob) + // more bad stuff + return false; + + // calculate current probability to achieve overall probability + // this should be nearly one + float prob2 = prob/m_currentProb; + // so rand() should have to return nearly RAND_MAX to get a positive + // remember, this is computing the probability that up to this point, one + // of these tests has returned positive, so each individual test has + // a necessarily low probability + bool res = (rand() > (int)(prob2 * RAND_MAX)); + m_currentProb = prob; + if (res) + LOG(VB_GENERAL, LOG_DEBUG, QString("$1 will run: this=$2; total=$3") + .arg(GetTag()).arg(prob2).arg(prob)); + else + LOG(VB_GENERAL, LOG_DEBUG, QString("$1 will not run: this=$2; total=$3") + .arg(GetTag()).arg(prob2).arg(prob)); + return res; +} + +bool PeriodicHouseKeeperTask::InWindow(QDateTime now) +{ + int elapsed = GetLastRun().secsTo(now); + + if (elapsed < 0) + // something bad has happened. let's just move along + return false; + + if ((elapsed > m_windowElapsed.first) && + (elapsed < m_windowElapsed.second)) + return true; + + return false; +} + +DailyHouseKeeperTask::DailyHouseKeeperTask(const QString &dbTag, + HouseKeeperScope scope, HouseKeeperStartup startup) : + PeriodicHouseKeeperTask(dbTag, 86400, .5, 1.5, scope, startup), + m_windowHour(0, 23) +{ + CalculateWindow(); +} + +DailyHouseKeeperTask::DailyHouseKeeperTask(const QString &dbTag, int minhour, + int maxhour, HouseKeeperScope scope, HouseKeeperStartup startup) : + PeriodicHouseKeeperTask(dbTag, 86400, .5, 1.5, scope, startup), + m_windowHour(minhour, maxhour) +{ + CalculateWindow(); +} + +void DailyHouseKeeperTask::CalculateWindow(void) +{ + PeriodicHouseKeeperTask::CalculateWindow(); + QDate date = GetLastRun().addDays(1).date(); + + QDateTime tmp = QDateTime(date, QTime(m_windowHour.first, 0)); + if (GetLastRun().addSecs(m_windowElapsed.first) < tmp) + m_windowElapsed.first = GetLastRun().secsTo(tmp); + + tmp = QDateTime(date, QTime(m_windowHour.second, 30)); + // we want to make sure this gets run before the end of the day + // so add a 30 minute buffer prior to the end of the window + if (GetLastRun().addSecs(m_windowElapsed.second) > tmp) + m_windowElapsed.second = GetLastRun().secsTo(tmp); +} + +void DailyHouseKeeperTask::SetHourWindow(int min, int max) +{ + m_windowHour.first = min; + m_windowHour.second = max; + CalculateWindow(); +} + +void HouseKeepingThread::run(void) +{ + m_waitMutex.lock(); + HouseKeeperTask *task = NULL; + + while (m_keepRunning) + { + m_idle = false; + + while ((task = m_parent->GetQueuedTask())) + { + // pull task from housekeeper and process it + ReferenceLocker rlock(task); + + if (!task->ConfirmRun()) + { + // something else has caused the lastrun time to + // change since this was requested to run. abort. + task = NULL; + continue; + } + + task->UpdateLastRun(); + task->Run(); + task = NULL; + + if (!m_keepRunning) + // thread has been discarded, don't try to start another task + break; + } + + m_idle = true; + + if (!m_keepRunning) + // short out rather than potentially hitting another sleep cycle + break; + + m_waitCondition.wait(&m_waitMutex); + } + + m_waitMutex.unlock(); +} + +HouseKeeper::HouseKeeper(void) : m_timer(NULL) +{ + m_timer = new QTimer(this); + connect(m_timer, SIGNAL(timeout()), this, SLOT(Run())); + m_timer->setInterval(60000); + m_timer->setSingleShot(false); +} + +HouseKeeper::~HouseKeeper(void) +{ + if (m_timer) + { + m_timer->stop(); + disconnect(m_timer); + delete m_timer; + m_timer = NULL; + } + + { + // remove anything from the queue first, so it does not start + QMutexLocker queueLock(&m_queueLock); + while (!m_taskQueue.isEmpty()) + m_taskQueue.takeFirst()->DecrRef(); + } + + { + // issue a terminate call to any long-running tasks + // this is just a noop unless overwritten by a subclass + QMutexLocker mapLock(&m_mapLock); + QMap::iterator it = m_taskMap.begin(); + for (; it != m_taskMap.end(); ++it) + (*it)->Terminate(); + } + + if (!m_threadList.isEmpty()) + { + QMutexLocker threadLock(&m_threadLock); + // tell primary thread to self-terminate and wake it + m_threadList.first()->Discard(); + m_threadList.first()->Wake(); + // wait for any remaining threads to self-terminate and close + while (!m_threadList.isEmpty()) + { + HouseKeepingThread *thread = m_threadList.takeFirst(); + thread->wait(); + delete thread; + } + } + + { + // unload any registered tasks + QMutexLocker mapLock(&m_mapLock); + QMap::iterator it = m_taskMap.begin(); + while (it != m_taskMap.end()) + { + (*it)->DecrRef(); + it = m_taskMap.erase(it); + } + } +} + +void HouseKeeper::RegisterTask(HouseKeeperTask *task) +{ + QMutexLocker mapLock(&m_mapLock); + QString tag = task->GetTag(); + if (m_taskMap.contains(tag)) + { + task->DecrRef(); + LOG(VB_GENERAL, LOG_ERR, + QString("HouseKeeperTask '%1' already registered. " + "Rejecting duplicate.").arg(tag)); + } + else + { + LOG(VB_GENERAL, LOG_INFO, + QString("Registering HouseKeeperTask '%1'.").arg(tag)); + m_taskMap.insert(tag, task); + } +} + +HouseKeeperTask* HouseKeeper::GetQueuedTask(void) +{ + QMutexLocker queueLock(&m_queueLock); + HouseKeeperTask *task = NULL; + + if (!m_taskQueue.isEmpty()) + { + task = m_taskQueue.dequeue(); + task->IncrRef(); + } + + // returning NULL tells the thread that the queue is empty and + // to go into standby + return task; +} + +void HouseKeeper::Start(void) +{ + // no need to be fine grained, nothing else should be accessing this map + QMutexLocker mapLock(&m_mapLock); + + if (m_timer->isActive()) + // Start() should only be called once + return; + + MSqlQuery query(MSqlQuery::InitCon()); + query.prepare("SELECT tag,lastrun" + " FROM housekeeping" + " WHERE hostname = :HOST" + " OR hostname IS NULL"); + query.bindValue(":HOST", gCoreContext->GetHostName()); + + if (!query.exec()) + MythDB::DBError("HouseKeeper::Run", query); + else + { + while (query.next()) + { + // loop through housekeeping table and load last run timestamps + QString tag = query.value(0).toString(); + QDateTime lastrun = MythDate::as_utc(query.value(1).toDateTime()); + + if (m_taskMap.contains(tag)) + m_taskMap[tag]->SetLastRun(lastrun); + } + } + + gCoreContext->addListener(this); + + QMap::const_iterator it; + for (it = m_taskMap.begin(); it != m_taskMap.end(); ++it) + { + if ((*it)->CheckImmediate()) + { + // run any tasks marked for immediate operation in-thread + (*it)->UpdateLastRun(); + (*it)->Run(); + } + else if ((*it)->CheckStartup()) + { + // queue any tasks marked for startup + LOG(VB_GENERAL, LOG_INFO, + QString("Queueing HouseKeeperTask '%1'.").arg(it.key())); + QMutexLocker queueLock(&m_queueLock); + (*it)->IncrRef(); + m_taskQueue.enqueue(*it); + } + } + + LOG(VB_GENERAL, LOG_INFO, "Starting HouseKeeper."); + + if (!m_taskQueue.isEmpty()) + StartThread(); + + m_timer->start(); +} + +void HouseKeeper::Run(void) +{ + LOG(VB_GENERAL, LOG_DEBUG, "Running HouseKeeper."); + + QDateTime now = MythDate::current(); + + QMutexLocker mapLock(&m_mapLock); + QMap::const_iterator it; + for (it = m_taskMap.begin(); it != m_taskMap.end(); ++it) + { + if ((*it)->CheckRun(now)) + { + // check if any tasks are ready to run, and add to queue + LOG(VB_GENERAL, LOG_INFO, + QString("Queueing HouseKeeperTask '%1'.").arg(it.key())); + QMutexLocker queueLock(&m_queueLock); + (*it)->IncrRef(); + m_taskQueue.enqueue(*it); + } + } + + if (!m_taskQueue.isEmpty()) + StartThread(); + + if (m_threadList.size() > 1) + { + // spent threads exist in the thread list + // check to see if any have finished up their task and terminated + QMutexLocker threadLock(&m_threadLock); + int count1 = m_threadList.size(); + + QList::iterator it = m_threadList.begin(); + ++it; // skip the primary thread + while (it != m_threadList.end()) + { + if ((*it)->isRunning()) + ++it; + else + it = m_threadList.erase(it); + } + + int count2 = m_threadList.size(); + if (count1 > count2) + LOG(VB_GENERAL, LOG_DEBUG, + QString("Discarded HouseKeepingThreads have completed and " + "been deleted. Current count %1 -> %2.") + .arg(count1).arg(count2)); + } +} + +void HouseKeeper::StartThread(void) +{ + QMutexLocker threadLock(&m_threadLock); + + if (m_threadList.isEmpty()) + { + // we're running for the first time + // start up a new thread + LOG(VB_GENERAL, LOG_DEBUG, "Running initial HouseKeepingThread."); + HouseKeepingThread *thread = new HouseKeepingThread(this); + m_threadList.append(thread); + thread->start(); + } + + else if (!m_threadList.first()->isIdle()) + { + // the old thread is still off processing something + // discard it and start a new one because we have more stuff + // that wants to run + LOG(VB_GENERAL, LOG_DEBUG, + QString("Current HouseKeepingThread is delayed on task, " + "spawning replacement. Current count %1.") + .arg(m_threadList.size())); + m_threadList.first()->Discard(); + HouseKeepingThread *thread = new HouseKeepingThread(this); + m_threadList.prepend(thread); + thread->start(); + } + + else + { + // the old thread is idle, so just wake it for processing + LOG(VB_GENERAL, LOG_DEBUG, "Waking HouseKeepingThread."); + m_threadList.first()->Wake(); + } +} + +void HouseKeeper::customEvent(QEvent *e) +{ + if ((MythEvent::Type)(e->type()) == MythEvent::MythEventMessage) + { + MythEvent *me = (MythEvent*)e; + if (me->Message().left(20) == "HOUSE_KEEPER_RUNNING") + { + QStringList tokens = me->Message() + .split(" ", QString::SkipEmptyParts); + if (tokens.size() != 4) + return; + + QString hostname = tokens[1]; + QString tag = tokens[2]; + QDateTime last = MythDate::fromString(tokens[3]); + + QMutexLocker mapLock(&m_mapLock); + if (m_taskMap.contains(tag)) + m_taskMap[tag]->SetLastRun(last); + } + } +} + diff --git a/mythtv/libs/libmythbase/housekeeper.h b/mythtv/libs/libmythbase/housekeeper.h new file mode 100644 index 00000000000..a0eff93ef3e --- /dev/null +++ b/mythtv/libs/libmythbase/housekeeper.h @@ -0,0 +1,165 @@ +#ifndef HOUSEKEEPER_H_ +#define HOUSEKEEPER_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mthread.h" +#include "mythdate.h" +#include "mythevent.h" +#include "mythbaseexp.h" +#include "referencecounter.h" + +class Scheduler; +class HouseKeeper; + +enum HouseKeeperScope { + kHKGlobal = 0, + kHKLocal, + kHKInst +}; + +enum HouseKeeperStartup { + kHKNormal = 0, + kHKRunOnStartup, + kHKRunImmediateOnStartup +}; + +class MBASE_PUBLIC HouseKeeperTask : public ReferenceCounter +{ + public: + HouseKeeperTask(const QString &dbTag, HouseKeeperScope scope=kHKGlobal, + HouseKeeperStartup startup=kHKNormal); + ~HouseKeeperTask() {} + + bool CheckRun(QDateTime now); + bool Run(void); + bool ConfirmRun(void) { return m_confirm; } + + bool CheckImmediate(void); + bool CheckStartup(void); + + QString GetTag(void) { return m_dbTag; } + QDateTime GetLastRun(void) { return m_lastRun; } + QDateTime QueryLastRun(void); + QDateTime UpdateLastRun(void) + { return UpdateLastRun(MythDate::current()); } + virtual QDateTime UpdateLastRun(QDateTime last); + virtual void SetLastRun(QDateTime last) { m_lastRun = last; + m_confirm = false; } + + virtual bool DoCheckRun(QDateTime now) { return false; } + virtual bool DoRun(void) { return false; } + + virtual void Terminate(void) {} + + private: + + QString m_dbTag; + bool m_confirm; + HouseKeeperScope m_scope; + HouseKeeperStartup m_startup; + + QDateTime m_lastRun; +}; + +class MBASE_PUBLIC PeriodicHouseKeeperTask : public HouseKeeperTask +{ + public: + PeriodicHouseKeeperTask(const QString &dbTag, int period, float min=0.5, + float max=1.1, HouseKeeperScope scope=kHKGlobal, + HouseKeeperStartup startup=kHKNormal); + virtual bool DoCheckRun(QDateTime now); + virtual bool InWindow(QDateTime now); + virtual QDateTime UpdateLastRun(QDateTime last); + virtual void SetLastRun(QDateTime last); + virtual void SetWindow(float min, float max); + + protected: + virtual void CalculateWindow(void); + + int m_period; + QPair m_windowPercent; + QPair m_windowElapsed; + float m_currentProb; +}; + +class MBASE_PUBLIC DailyHouseKeeperTask : public PeriodicHouseKeeperTask +{ + public: + DailyHouseKeeperTask(const QString &dbTag, + HouseKeeperScope scope=kHKGlobal, + HouseKeeperStartup startup=kHKNormal); + DailyHouseKeeperTask(const QString &dbTag, int minhour, int maxhour, + HouseKeeperScope scope=kHKGlobal, + HouseKeeperStartup startup=kHKNormal); + virtual void SetHourWindow(int min, int max); + + protected: + virtual void CalculateWindow(void); + + private: + QPair m_windowHour; +}; + +class HouseKeepingThread : public MThread +{ + public: + HouseKeepingThread(HouseKeeper *p) : + MThread("HouseKeeping"), m_idle(true), m_keepRunning(true), + m_parent(p) {} + ~HouseKeepingThread() {} + virtual void run(void); + void Discard(void) { m_keepRunning = false; } + bool isIdle(void) { return m_idle; } + void Wake(void) { m_waitCondition.wakeAll(); } + + void Terminate(void); + + private: + bool m_idle; + bool m_keepRunning; + HouseKeeper *m_parent; + QMutex m_waitMutex; + QWaitCondition m_waitCondition; +}; + +class MBASE_PUBLIC HouseKeeper : public QObject +{ + Q_OBJECT + + public: + HouseKeeper(void); + ~HouseKeeper(); + + void RegisterTask(HouseKeeperTask *task); + void Start(void); + void StartThread(void); + HouseKeeperTask* GetQueuedTask(void); + + void customEvent(QEvent *e); + + public slots: + void Run(void); + + private: + QTimer *m_timer; + + QQueue m_taskQueue; + QMutex m_queueLock; + + QMap m_taskMap; + QMutex m_mapLock; + + QList m_threadList; + QMutex m_threadLock; +}; + +#endif diff --git a/mythtv/libs/libmythbase/libmythbase.pro b/mythtv/libs/libmythbase/libmythbase.pro index 090ccfb3c31..a16a479baa9 100644 --- a/mythtv/libs/libmythbase/libmythbase.pro +++ b/mythtv/libs/libmythbase/libmythbase.pro @@ -26,7 +26,7 @@ HEADERS += mythdeque.h mythlogging.h HEADERS += mythbaseutil.h referencecounter.h version.h mythcommandlineparser.h HEADERS += mythscheduler.h filesysteminfo.h hardwareprofile.h serverpool.h HEADERS += plist.h bswap.h signalhandling.h mythtimezone.h mythdate.h -HEADERS += mythplugin.h mythpluginapi.h +HEADERS += mythplugin.h mythpluginapi.h housekeeper.h HEADERS += ffmpeg-mmx.h SOURCES += mthread.cpp mthreadpool.cpp @@ -43,7 +43,7 @@ SOURCES += logging.cpp loggingserver.cpp SOURCES += referencecounter.cpp mythcommandlineparser.cpp SOURCES += filesysteminfo.cpp hardwareprofile.cpp serverpool.cpp SOURCES += plist.cpp signalhandling.cpp mythtimezone.cpp mythdate.cpp -SOURCES += mythplugin.cpp +SOURCES += mythplugin.cpp housekeeper.cpp # This stuff is not Qt5 compatible.. contains(QT_VERSION, ^4\\.[0-9]\\..*) { diff --git a/mythtv/programs/mythbackend/backendhousekeeper.cpp b/mythtv/programs/mythbackend/backendhousekeeper.cpp new file mode 100644 index 00000000000..b5c597edca7 --- /dev/null +++ b/mythtv/programs/mythbackend/backendhousekeeper.cpp @@ -0,0 +1,565 @@ +// POSIX headers +#include +#include + +// ANSI C headers +#include + +// Qt headers +#include +#include +#include +#include + +// MythTV headers +#include "backendhousekeeper.h" +#include "mythdb.h" +#include "mythdirs.h" +#include "jobqueue.h" +#include "exitcodes.h" +#include "mythsystem.h" +#include "mythversion.h" +#include "mythcoreutil.h" +#include "programtypes.h" +#include "recordingtypes.h" +#include "mythcorecontext.h" +#include "mythdownloadmanager.h" + + +bool LogCleanerTask::DoRun(void) +{ + int numdays = 14; + uint64_t maxrows = 10000 * numdays; // likely high enough to keep numdays + bool res = true; + + MSqlQuery query(MSqlQuery::InitCon()); + if (query.isConnected()) + { + // Remove less-important logging after 1/2 * numdays days + QDateTime days = MythDate::current(); + days = days.addDays(0 - (numdays / 2)); + QString sql = "DELETE FROM logging " + " WHERE application NOT IN (:MYTHBACKEND, :MYTHFRONTEND) " + " AND msgtime < :DAYS ;"; + query.prepare(sql); + query.bindValue(":MYTHBACKEND", MYTH_APPNAME_MYTHBACKEND); + query.bindValue(":MYTHFRONTEND", MYTH_APPNAME_MYTHFRONTEND); + query.bindValue(":DAYS", days); + LOG(VB_GENERAL, LOG_DEBUG, + QString("Deleting helper application database log entries " + "from before %1.") .arg(days.toString(Qt::ISODate))); + if (!query.exec()) + { + MythDB::DBError("Delete helper application log entries", query); + res = false; + } + + // Remove backend/frontend logging after numdays days + days = MythDate::current(); + days = days.addDays(0 - numdays); + sql = "DELETE FROM logging WHERE msgtime < :DAYS ;"; + query.prepare(sql); + query.bindValue(":DAYS", days); + LOG(VB_GENERAL, LOG_DEBUG, + QString("Deleting database log entries from before %1.") + .arg(days.toString(Qt::ISODate))); + if (!query.exec()) + { + MythDB::DBError("Delete old log entries", query); + res = false; + } + + sql = "SELECT COUNT(id) FROM logging;"; + query.prepare(sql); + if (query.exec()) + { + uint64_t totalrows = 0; + while (query.next()) + { + totalrows = query.value(0).toLongLong(); + LOG(VB_GENERAL, LOG_DEBUG, + QString("Database has %1 log entries.").arg(totalrows)); + } + if (totalrows > maxrows) + { + sql = "DELETE FROM logging ORDER BY msgtime LIMIT :ROWS;"; + query.prepare(sql); + quint64 extrarows = totalrows - maxrows; + query.bindValue(":ROWS", extrarows); + LOG(VB_GENERAL, LOG_DEBUG, + QString("Deleting oldest %1 database log entries.") + .arg(extrarows)); + if (!query.exec()) + { + MythDB::DBError("Delete excess log entries", query); + res = false; + } + } + } + else + { + MythDB::DBError("Query logging table size", query); + res = false; + } + } + + return res; +} + +bool CleanupTask::DoRun(void) +{ + JobQueue::CleanupOldJobsInQueue(); + CleanupOldRecordings(); + CleanupInUsePrograms(); + CleanupOrphanedLiveTV(); + CleanupRecordedTables(); + CleanupProgramListings(); + return true; +} + +void CleanupTask::CleanupOldRecordings(void) +{ + MSqlQuery query(MSqlQuery::InitCon()); + + query.prepare("DELETE FROM inuseprograms " + "WHERE hostname = :HOSTNAME AND " + "( recusage = 'recorder' OR recusage LIKE 'Unknown %' );"); + query.bindValue(":HOSTNAME", gCoreContext->GetHostName()); + if (!query.exec()) + MythDB::DBError("CleanupTask::CleanupOldRecordings", query); +} + +void CleanupTask::CleanupInUsePrograms(void) +{ + QDateTime fourHoursAgo = MythDate::current().addSecs(-4 * 60 * 60); + MSqlQuery query(MSqlQuery::InitCon()); + + query.prepare("DELETE FROM inuseprograms " + "WHERE lastupdatetime < :FOURHOURSAGO ;"); + query.bindValue(":FOURHOURSAGO", fourHoursAgo); + if (!query.exec()) + MythDB::DBError("CleanupTask::CleanupInUsePrograms", query); +} + +void CleanupTask::CleanupOrphanedLiveTV(void) +{ + QDateTime fourHoursAgo = MythDate::current().addSecs(-4 * 60 * 60); + MSqlQuery query(MSqlQuery::InitCon()); + MSqlQuery deleteQuery(MSqlQuery::InitCon()); + + // Keep these tvchains, they may be in use. + query.prepare("SELECT DISTINCT chainid FROM tvchain " + "WHERE endtime > :FOURHOURSAGO ;"); + query.bindValue(":FOURHOURSAGO", fourHoursAgo); + + if (!query.exec() || !query.isActive()) + { + MythDB::DBError("HouseKeeper Cleaning TVChain Table", query); + return; + } + + QString msg, keepChains; + while (query.next()) + if (keepChains.isEmpty()) + keepChains = "'" + query.value(0).toString() + "'"; + else + keepChains += ", '" + query.value(0).toString() + "'"; + + if (keepChains.isEmpty()) + msg = "DELETE FROM tvchain WHERE endtime < now();"; + else + { + msg = QString("DELETE FROM tvchain " + "WHERE chainid NOT IN ( %1 ) AND endtime < now();") + .arg(keepChains); + } + deleteQuery.prepare(msg); + if (!deleteQuery.exec()) + MythDB::DBError("CleanupTask::CleanupOrphanedLiveTV", deleteQuery); +} + +void CleanupTask::CleanupRecordedTables(void) +{ + MSqlQuery query(MSqlQuery::InitCon()); + MSqlQuery deleteQuery(MSqlQuery::InitCon()); + int tableIndex = 0; + // tables[tableIndex][0] is the table name + // tables[tableIndex][1] is the name of the column on which the join is + // performed + QString tables[][2] = { + { "recordedprogram", "progstart" }, + { "recordedrating", "progstart" }, + { "recordedcredits", "progstart" }, + { "recordedmarkup", "starttime" }, + { "recordedseek", "starttime" }, + { "", "" } }; // This blank entry must exist, do not remove. + QString table = tables[tableIndex][0]; + QString column = tables[tableIndex][1]; + + // Because recordedseek can have millions of rows, we don't want to JOIN it + // with recorded. Instead, pull out DISTINCT chanid and starttime into a + // temporary table (resulting in tens, hundreds, or--at most--a few + // thousand rows) for the JOIN + QString querystr; + querystr = "CREATE TEMPORARY TABLE IF NOT EXISTS temprecordedcleanup ( " + "chanid int(10) unsigned NOT NULL default '0', " + "starttime datetime NOT NULL default '0000-00-00 00:00:00' " + ");"; + + if (!query.exec(querystr)) + { + MythDB::DBError("CleanupTask::CleanupRecordedTables" + "(creating temporary table)", query); + return; + } + + while (!table.isEmpty()) + { + query.prepare(QString("TRUNCATE TABLE temprecordedcleanup;")); + if (!query.exec() || !query.isActive()) + { + MythDB::DBError("CleanupTask::CleanupRecordedTables" + "(truncating temporary table)", query); + return; + } + + query.prepare(QString("INSERT INTO temprecordedcleanup " + "( chanid, starttime ) " + "SELECT DISTINCT chanid, starttime " + "FROM %1;") + .arg(table)); + + if (!query.exec() || !query.isActive()) + { + MythDB::DBError("CleanupTask::CleanupRecordedTables" + "(cleaning recorded tables)", query); + return; + } + + query.prepare(QString("SELECT DISTINCT p.chanid, p.starttime " + "FROM temprecordedcleanup p " + "LEFT JOIN recorded r " + "ON p.chanid = r.chanid " + "AND p.starttime = r.%1 " + "WHERE r.chanid IS NULL;").arg(column)); + if (!query.exec() || !query.isActive()) + { + MythDB::DBError("CleanupTask::CleanupRecordedTables" + "(cleaning recorded tables)", query); + return; + } + + deleteQuery.prepare(QString("DELETE FROM %1 " + "WHERE chanid = :CHANID " + "AND starttime = :STARTTIME;") + .arg(table)); + while (query.next()) + { + deleteQuery.bindValue(":CHANID", query.value(0).toString()); + deleteQuery.bindValue(":STARTTIME", query.value(1)); + if (!deleteQuery.exec()) + { + MythDB::DBError("CleanupTask::CleanupRecordedTables" + "(cleaning recorded tables)", deleteQuery); + return; + } + } + + tableIndex++; + table = tables[tableIndex][0]; + column = tables[tableIndex][1]; + } + + if (!query.exec("DROP TABLE temprecordedcleanup;")) + MythDB::DBError("CleanupTask::CleanupRecordedTables" + "(deleting temporary table)", query); +} + +void CleanupTask::CleanupProgramListings(void) +{ + MSqlQuery query(MSqlQuery::InitCon()); + QString querystr; + // Keep as many days of listings data as we keep matching, non-recorded + // oldrecorded entries to allow for easier post-mortem analysis + int offset = gCoreContext->GetNumSetting( "CleanOldRecorded", 10); + // Also make sure to keep enough data so that we can flag the original + // airdate, for when that isn't included in guide data + int newEpiWindow = gCoreContext->GetNumSetting( "NewEpisodeWindow", 14); + if (newEpiWindow > offset) + offset = newEpiWindow; + + query.prepare("DELETE FROM oldprogram WHERE airdate < " + "DATE_SUB(CURRENT_DATE, INTERVAL 320 DAY);"); + if (!query.exec()) + MythDB::DBError("HouseKeeper Cleaning Program Listings", query); + + query.prepare("REPLACE INTO oldprogram (oldtitle,airdate) " + "SELECT title,starttime FROM program " + "WHERE starttime < NOW() AND manualid = 0 " + "GROUP BY title;"); + if (!query.exec()) + MythDB::DBError("HouseKeeper Cleaning Program Listings", query); + + query.prepare("DELETE FROM program WHERE starttime <= " + "DATE_SUB(CURRENT_DATE, INTERVAL :OFFSET DAY);"); + query.bindValue(":OFFSET", offset); + if (!query.exec()) + MythDB::DBError("HouseKeeper Cleaning Program Listings", query); + + query.prepare("DELETE FROM programrating WHERE starttime <= " + "DATE_SUB(CURRENT_DATE, INTERVAL :OFFSET DAY);"); + query.bindValue(":OFFSET", offset); + if (!query.exec()) + MythDB::DBError("HouseKeeper Cleaning Program Listings", query); + + query.prepare("DELETE FROM programgenres WHERE starttime <= " + "DATE_SUB(CURRENT_DATE, INTERVAL :OFFSET DAY);"); + query.bindValue(":OFFSET", offset); + if (!query.exec()) + MythDB::DBError("HouseKeeper Cleaning Program Listings", query); + + query.prepare("DELETE FROM credits WHERE starttime <= " + "DATE_SUB(CURRENT_DATE, INTERVAL :OFFSET DAY);"); + query.bindValue(":OFFSET", offset); + if (!query.exec()) + MythDB::DBError("HouseKeeper Cleaning Program Listings", query); + + query.prepare("DELETE FROM record WHERE (type = :SINGLE " + "OR type = :OVERRIDE OR type = :DONTRECORD) " + "AND enddate < CURDATE();"); + query.bindValue(":SINGLE", kSingleRecord); + query.bindValue(":OVERRIDE", kOverrideRecord); + query.bindValue(":DONTRECORD", kDontRecord); + if (!query.exec()) + MythDB::DBError("HouseKeeper Cleaning Program Listings", query); + + MSqlQuery findq(MSqlQuery::InitCon()); + findq.prepare("SELECT record.recordid FROM record " + "LEFT JOIN oldfind ON oldfind.recordid = record.recordid " + "WHERE type = :FINDONE AND oldfind.findid IS NOT NULL;"); + findq.bindValue(":FINDONE", kOneRecord); + + if (findq.exec()) + { + query.prepare("DELETE FROM record WHERE recordid = :RECORDID;"); + while (findq.next()) + { + query.bindValue(":RECORDID", findq.value(0).toInt()); + if (!query.exec()) + MythDB::DBError("HouseKeeper Cleaning Program Listings", query); + } + } + query.prepare("DELETE FROM oldfind WHERE findid < TO_DAYS(NOW()) - 14;"); + if (!query.exec()) + MythDB::DBError("HouseKeeper Cleaning Program Listings", query); + + query.prepare("DELETE FROM oldrecorded WHERE " + "recstatus <> :RECORDED AND duplicate = 0 AND " + "endtime < DATE_SUB(CURRENT_DATE, INTERVAL :CLEAN DAY);"); + query.bindValue(":RECORDED", rsRecorded); + query.bindValue(":CLEAN", offset); + if (!query.exec()) + MythDB::DBError("HouseKeeper Cleaning Program Listings", query); +} + +bool ThemeUpdateTask::DoRun(void) +{ + QString MythVersion = MYTH_SOURCE_PATH; + + // FIXME: For now, treat git master the same as svn trunk + if (MythVersion == "master") + MythVersion = "trunk"; + + if (MythVersion != "trunk") + { + MythVersion = MYTH_BINARY_VERSION; // Example: 0.25.20101017-1 + MythVersion.replace(QRegExp("\\.[0-9]{8,}.*"), ""); + } + + QString remoteThemesDir = GetConfDir(); + remoteThemesDir.append("/tmp/remotethemes"); + + QDir dir(remoteThemesDir); + if (!dir.exists() && !dir.mkpath(remoteThemesDir)) + { + LOG(VB_GENERAL, LOG_ERR, + QString("HouseKeeper: Error creating %1" + "directory for remote themes info cache.") + .arg(remoteThemesDir)); + return false; + } + + QString remoteThemesFile = remoteThemesDir; + remoteThemesFile.append("/themes.zip"); + + QString url = QString("%1/%2/themes.zip") + .arg(gCoreContext->GetSetting("ThemeRepositoryURL", + "http://themes.mythtv.org/themes/repository")).arg(MythVersion); + + bool result = GetMythDownloadManager()->download(url, remoteThemesFile); + + if (!result) + { + LOG(VB_GENERAL, LOG_ERR, + QString("HouseKeeper: Error downloading %1" + "remote themes info package.").arg(url)); + return false; + } + + if (!extractZIP(remoteThemesFile, remoteThemesDir)) + { + LOG(VB_GENERAL, LOG_ERR, + QString("HouseKeeper: Error extracting %1" + "remote themes info package.").arg(remoteThemesFile)); + QFile::remove(remoteThemesFile); + return false; + } + + return true; +} + +bool ArtworkTask::DoRun(void) +{ + QString command = GetInstallPrefix() + "/bin/mythmetadatalookup"; + QStringList args; + args << "--refresh-all-artwork"; + args << logPropagateArgs; + + LOG(VB_GENERAL, LOG_INFO, QString("Performing Artwork Refresh: %1 %2") + .arg(command).arg(args.join(" "))); + + MythSystem artupd(command, args, kMSRunShell | kMSAutoCleanup); + + artupd.Run(); + artupd.Wait(); + + LOG(VB_GENERAL, LOG_INFO, QString("Artwork Refresh Complete")); + return true; +} + +bool JobQueueRecoverTask::DoRun(void) +{ + JobQueue::RecoverOldJobsInQueue(); + return true; +} + +MythFillDatabaseTask::MythFillDatabaseTask(void) : + DailyHouseKeeperTask("MythFillDB"), m_msMFD(NULL) +{ + // we need to set the time window from database settings, so we cannot + // initialize these values in. grab them and set them afterwards + int min = gCoreContext->GetNumSetting("MythFillMinHour", -1); + int max = gCoreContext->GetNumSetting("MythFillMaxHour", 23); + + if (min == -1) + { + min = 0; + max = 23; + } + else + { + // make sure they fall within the range of 0-23 + min %= 24; + max %= 24; + } + + SetHourWindow(min, max); +} + +bool MythFillDatabaseTask::UseSuggestedTime(void) +{ + if (!gCoreContext->GetNumSetting("MythFillGrabberSuggestsTime", 1)) + // this feature is disabled, so don't bother with a deeper check + return false; + + MSqlQuery result(MSqlQuery::InitCon()); + if (result.isConnected()) + { + // check to see if we have any of a list of supported grabbers in use + // TODO: this is really cludgy. there has to be a better way to test + result.prepare("SELECT COUNT(*) FROM videosource" + " WHERE xmltvgrabber IN" + " ( 'datadirect'," + " 'technovera'," + " 'schedulesdirect1' );"); + if ((result.exec()) && + (result.next()) && + (result.value(0).toInt() > 0)) + return true; + } + + return false; +} + +bool MythFillDatabaseTask::DoCheckRun(QDateTime now) +{ + if (!gCoreContext->GetNumSetting("MythFillEnabled", 1)) + // we don't want to run this manually, so abort early + return false; + +// if (m_running) +// // we're still running from the previous pass, so abort early +// return false; + + if (UseSuggestedTime()) + { + QDateTime nextRun = MythDate::fromString( + gCoreContext->GetSetting("MythFillSuggestedRunTime", + "1970-01-01T00:00:00")); + if (nextRun > now) + // not yet time + return false; + + if (InWindow(now)) + // we're inside our permitted window + return true; + + return false; + } + else + // just let DailyHouseKeeperTask handle things + return DailyHouseKeeperTask::DoCheckRun(now); +} + +bool MythFillDatabaseTask::DoRun(void) +{ + if (m_msMFD) + { + // this should never be defined, but terminate it anyway + if (m_msMFD->GetStatus() == GENERIC_EXIT_RUNNING) + m_msMFD->Term(true); + delete m_msMFD; + m_msMFD = NULL; + } + + QString mfpath = gCoreContext->GetSetting("MythFillDatabasePath", + "mythfilldatabase"); + QString mfarg = gCoreContext->GetSetting("MythFillDatabaseArgs", ""); + + uint opts = kMSRunShell | kMSAutoCleanup; + if (mfpath == "mythfilldatabase") + opts |= kMSPropagateLogs; + + QString cmd = QString("%1 %2").arg(mfpath).arg(mfarg); + + m_msMFD = new MythSystem(cmd, opts); + m_msMFD->Run(); + uint result = m_msMFD->Wait(); + m_msMFD = NULL; + + if (result != GENERIC_EXIT_OK) + { + LOG(VB_GENERAL, LOG_ERR, QString("MythFillDatabase command '%1' failed") + .arg(cmd)); + return false; + } + + return true; +} + +void MythFillDatabaseTask::Terminate(void) +{ + if (m_msMFD && (m_msMFD->GetStatus() == GENERIC_EXIT_RUNNING)) + // just kill it, the runner thread will handle any necessary cleanup + m_msMFD->Term(true); +} diff --git a/mythtv/programs/mythbackend/backendhousekeeper.h b/mythtv/programs/mythbackend/backendhousekeeper.h new file mode 100644 index 00000000000..7996f08148e --- /dev/null +++ b/mythtv/programs/mythbackend/backendhousekeeper.h @@ -0,0 +1,74 @@ +#ifndef BACKENDHOUSEKEEPER_H_ +#define BACKENDHOUSEKEEPER_H_ + +#include "housekeeper.h" +#include "mythsystem.h" + +class LogCleanerTask : public DailyHouseKeeperTask +{ + public: + LogCleanerTask(void) : DailyHouseKeeperTask("LogCleanup", kHKGlobal) {}; + bool DoRun(void); +}; + + +class CleanupTask : public DailyHouseKeeperTask +{ + public: + CleanupTask(void) : DailyHouseKeeperTask("LogCleanup", kHKGlobal) {}; + bool DoRun(void); + + private: + void CleanupOldRecordings(void); + void CleanupInUsePrograms(void); + void CleanupOrphanedLiveTV(void); + void CleanupRecordedTables(void); + void CleanupProgramListings(void); +}; + + +class ThemeUpdateTask : public DailyHouseKeeperTask +{ + public: + ThemeUpdateTask(void) : DailyHouseKeeperTask("ThemeUpdateNotifications", + kHKGlobal, kHKRunOnStartup) {}; + bool DoRun(void); +}; + + +class ArtworkTask : public DailyHouseKeeperTask +{ + public: + ArtworkTask(void) : DailyHouseKeeperTask("RecordedArtworkUpdate", + kHKGlobal, kHKRunOnStartup) {}; + bool DoRun(void); +}; + + +class JobQueueRecoverTask : public DailyHouseKeeperTask +{ + public: + JobQueueRecoverTask(void) : DailyHouseKeeperTask("JobQueueRecover") {}; + bool DoRun(void); +}; + + +class MythFillDatabaseTask : public DailyHouseKeeperTask +{ + public: + MythFillDatabaseTask(void); + + static bool UseSuggestedTime(void); + + virtual bool DoCheckRun(QDateTime now); + virtual bool DoRun(void); + + virtual void Terminate(void); + private: + MythSystem *m_msMFD; +// bool m_running; +}; + + + +#endif diff --git a/mythtv/programs/mythbackend/main.cpp b/mythtv/programs/mythbackend/main.cpp index 5e30d2888b6..b70d0a01942 100644 --- a/mythtv/programs/mythbackend/main.cpp +++ b/mythtv/programs/mythbackend/main.cpp @@ -24,7 +24,6 @@ #include "main_helpers.h" #include "mythmiscutil.h" #include "storagegroup.h" -#include "housekeeper.h" #include "mediaserver.h" #include "mythlogging.h" #include "mythversion.h" diff --git a/mythtv/programs/mythbackend/main_helpers.cpp b/mythtv/programs/mythbackend/main_helpers.cpp index fd4c4dc854d..e7724f6bf21 100644 --- a/mythtv/programs/mythbackend/main_helpers.cpp +++ b/mythtv/programs/mythbackend/main_helpers.cpp @@ -30,7 +30,7 @@ #include "mainserver.h" #include "encoderlink.h" #include "remoteutil.h" -#include "housekeeper.h" +#include "backendhousekeeper.h" #include "mythcontext.h" #include "mythversion.h" @@ -600,9 +600,6 @@ int run_backend(MythBackendCommandLineParser &cmdline) sched->DisableScheduling(); } - if (!cmdline.toBool("nohousekeeper")) - housekeeping = new HouseKeeper(true, ismaster, sched); - if (!cmdline.toBool("noautoexpire")) { expirer = new AutoExpire(&tvList); @@ -611,9 +608,21 @@ int run_backend(MythBackendCommandLineParser &cmdline) } gCoreContext->SetScheduler(sched); } - else if (!cmdline.toBool("nohousekeeper")) + + if (!cmdline.toBool("nohousekeeper")) { - housekeeping = new HouseKeeper(true, ismaster, NULL); + housekeeping = new HouseKeeper(); + + if (ismaster) + { + housekeeping->RegisterTask(new LogCleanerTask()); + housekeeping->RegisterTask(new CleanupTask()); + housekeeping->RegisterTask(new ThemeUpdateTask()); + housekeeping->RegisterTask(new ArtworkTask()); + housekeeping->RegisterTask(new MythFillDatabaseTask()); + } + + housekeeping->RegisterTask(new JobQueueRecoverTask()); } if (!cmdline.toBool("nojobqueue")) diff --git a/mythtv/programs/mythbackend/mythbackend.pro b/mythtv/programs/mythbackend/mythbackend.pro index b04a05df145..b7b3ed94866 100644 --- a/mythtv/programs/mythbackend/mythbackend.pro +++ b/mythtv/programs/mythbackend/mythbackend.pro @@ -22,7 +22,8 @@ QMAKE_CLEAN += $(TARGET) # Input HEADERS += autoexpire.h encoderlink.h filetransfer.h httpstatus.h mainserver.h -HEADERS += playbacksock.h scheduler.h server.h housekeeper.h backendutil.h +HEADERS += playbacksock.h scheduler.h server.h backendhousekeeper.h +HEADERS += backendutil.h HEADERS += upnpcdstv.h upnpcdsmusic.h upnpcdsvideo.h mediaserver.h HEADERS += internetContent.h main_helpers.h backendcontext.h HEADERS += httpconfig.h mythsettings.h commandlineparser.h @@ -38,7 +39,7 @@ HEADERS += services/capture.h SOURCES += autoexpire.cpp encoderlink.cpp filetransfer.cpp httpstatus.cpp SOURCES += main.cpp mainserver.cpp playbacksock.cpp scheduler.cpp server.cpp -SOURCES += housekeeper.cpp backendutil.cpp +SOURCES += backendhousekeeper.cpp backendutil.cpp SOURCES += upnpcdstv.cpp upnpcdsmusic.cpp upnpcdsvideo.cpp mediaserver.cpp SOURCES += internetContent.cpp main_helpers.cpp backendcontext.cpp SOURCES += httpconfig.cpp mythsettings.cpp commandlineparser.cpp