MythTV  0.26-pre
weatherSource.cpp
Go to the documentation of this file.
00001 #include <unistd.h>
00002 
00003 // QT headers
00004 #include <QDir>
00005 #include <QFile>
00006 #include <QTextStream>
00007 #include <QTextCodec>                                                          
00008 #include <QApplication>
00009 
00010 // MythTV headers
00011 #include <mythcontext.h>
00012 #include <mythdb.h>
00013 #include <compat.h>
00014 #include <mythdirs.h>
00015 #include <mythsystem.h>
00016 #include <exitcodes.h>
00017 
00018 // MythWeather headers
00019 #include "weatherScreen.h"
00020 #include "weatherSource.h"
00021 
00022 QStringList WeatherSource::ProbeTypes(QString workingDirectory,
00023                                       QString program)
00024 {
00025     QStringList arguments("-t");
00026     const QString loc = QString("WeatherSource::ProbeTypes(%1 %2): ")
00027         .arg(program).arg(arguments.join(" "));
00028     QStringList types;
00029 
00030     uint flags = kMSRunShell | kMSStdOut | kMSBuffered | 
00031                  kMSDontDisableDrawing | kMSDontBlockInputDevs;
00032     MythSystem ms(program, arguments, flags);
00033     ms.SetDirectory(workingDirectory);
00034     ms.Run();
00035     if (ms.Wait() != GENERIC_EXIT_OK)
00036     {
00037         LOG(VB_GENERAL, LOG_ERR, loc + "Cannot run script");
00038         return types;
00039     }
00040 
00041     QByteArray result = ms.ReadAll();
00042     QTextStream text(result);
00043 
00044     while (!text.atEnd())
00045     {
00046         QString tmp = text.readLine();
00047 
00048         while (tmp.endsWith('\n') || tmp.endsWith('\r'))
00049             tmp.chop(1);
00050 
00051         if (!tmp.isEmpty())
00052             types += tmp;
00053     }
00054 
00055     if (types.empty())
00056         LOG(VB_GENERAL, LOG_ERR, loc + "Invalid output from -t option");
00057 
00058     return types;
00059 }
00060 
00061 bool WeatherSource::ProbeTimeouts(QString  workingDirectory,
00062                                   QString  program,
00063                                   uint    &updateTimeout,
00064                                   uint    &scriptTimeout)
00065 {
00066     QStringList arguments("-T");
00067     const QString loc = QString("WeatherSource::ProbeTimeouts(%1 %2): ")
00068         .arg(program).arg(arguments.join(" "));
00069 
00070     updateTimeout = DEFAULT_UPDATE_TIMEOUT;
00071     scriptTimeout = DEFAULT_SCRIPT_TIMEOUT;
00072 
00073     uint flags = kMSRunShell | kMSStdOut | kMSBuffered | 
00074                  kMSDontDisableDrawing | kMSDontBlockInputDevs;
00075     MythSystem ms(program, arguments, flags);
00076     ms.SetDirectory(workingDirectory);
00077     ms.Run();
00078     if (ms.Wait() != GENERIC_EXIT_OK)
00079     {
00080         LOG(VB_GENERAL, LOG_ERR, loc + "Cannot run script");
00081         return false;
00082     }
00083 
00084     QByteArray result = ms.ReadAll();
00085     QTextStream text(result);
00086 
00087     QStringList lines;
00088     while (!text.atEnd())
00089     {
00090         QString tmp = text.readLine();
00091 
00092         while (tmp.endsWith('\n') || tmp.endsWith('\r'))
00093             tmp.chop(1);
00094 
00095         if (!tmp.isEmpty())
00096             lines << tmp;
00097     }
00098 
00099     if (lines.empty())
00100     {
00101         LOG(VB_GENERAL, LOG_ERR, loc + "Invalid Script Output! No Lines");
00102         return false;
00103     }
00104 
00105     QStringList temp = lines[0].split(',');
00106     if (temp.size() != 2)
00107     {
00108         LOG(VB_GENERAL, LOG_ERR, loc +
00109             QString("Invalid Script Output! '%1'").arg(lines[0]));
00110         return false;
00111     }
00112 
00113     bool isOK[2];
00114     uint ut = temp[0].toUInt(&isOK[0]);
00115     uint st = temp[1].toUInt(&isOK[1]);
00116     if (!isOK[0] || !isOK[1])
00117     {
00118         LOG(VB_GENERAL, LOG_ERR, loc +
00119             QString("Invalid Script Output! '%1'").arg(lines[0]));
00120         return false;
00121     }
00122 
00123     updateTimeout = ut * 1000;
00124     scriptTimeout = st;
00125 
00126     return true;
00127 }
00128 
00129 bool WeatherSource::ProbeInfo(ScriptInfo &info)
00130 {
00131     QStringList arguments("-v");
00132 
00133     const QString loc = QString("WeatherSource::ProbeInfo(%1 %2): ")
00134         .arg(info.program).arg(arguments.join(" "));
00135 
00136     uint flags = kMSRunShell | kMSStdOut | kMSBuffered | 
00137                  kMSDontDisableDrawing | kMSDontBlockInputDevs;
00138     MythSystem ms(info.program, arguments, flags);
00139     ms.SetDirectory(info.path);
00140     ms.Run();
00141     if (ms.Wait() != GENERIC_EXIT_OK)
00142     {
00143         LOG(VB_GENERAL, LOG_ERR, loc + "Cannot run script");
00144         return false;
00145     }
00146 
00147     QByteArray result = ms.ReadAll();
00148     QTextStream text(result);
00149 
00150     QStringList lines;
00151     while (!text.atEnd())
00152     {
00153         QString tmp = text.readLine();
00154 
00155         while (tmp.endsWith('\n') || tmp.endsWith('\r'))
00156             tmp.chop(1);
00157 
00158         if (!tmp.isEmpty())
00159             lines << tmp;
00160     }
00161 
00162     if (lines.empty())
00163     {
00164         LOG(VB_GENERAL, LOG_ERR, loc + "Invalid Script Output! No Lines");
00165         return false;
00166     }
00167 
00168     QStringList temp = lines[0].split(',');
00169     if (temp.size() != 4)
00170     {
00171         LOG(VB_GENERAL, LOG_ERR, loc +
00172             QString("Invalid Script Output! '%1'").arg(lines[0]));
00173         return false;
00174     }
00175 
00176     info.name    = temp[0];
00177     info.version = temp[1];
00178     info.author  = temp[2];
00179     info.email   = temp[3];
00180 
00181     return true;
00182 }
00183 
00184 /* Basic logic of this behemouth...
00185  * run script with -v flag, this returns among other things, the version number
00186  * Search the database using the name (also returned from -v).
00187  * if it exists, compare versions from -v and db
00188  * if the same, populate the info struct from db, and we're done
00189  * if they differ, get the rest of the needed information from the script and
00190  * update the database, note, it does not overwrite the existing timeout values.
00191  * if the script is not in the database, we probe it for types and default
00192  * timeout values, and add it to the database
00193  */
00194 ScriptInfo *WeatherSource::ProbeScript(const QFileInfo &fi)
00195 {
00196     QStringList temp;
00197 
00198     if (!fi.isReadable() || !fi.isExecutable())
00199         return NULL;
00200 
00201     ScriptInfo info;
00202     info.path = fi.absolutePath();
00203     info.program = fi.absoluteFilePath();
00204 
00205     if (!WeatherSource::ProbeInfo(info))
00206         return NULL;
00207 
00208     MSqlQuery db(MSqlQuery::InitCon());
00209     QString query =
00210             "SELECT sourceid, source_name, update_timeout, retrieve_timeout, "
00211             "path, author, version, email, types FROM weathersourcesettings "
00212             "WHERE hostname = :HOST AND source_name = :NAME;";
00213     db.prepare(query);
00214     db.bindValue(":HOST", gCoreContext->GetHostName());
00215     db.bindValue(":NAME", info.name);
00216 
00217     if (!db.exec())
00218     {
00219         LOG(VB_GENERAL, LOG_ERR, "Invalid response from database");
00220         return NULL;
00221     }
00222 
00223     // the script exists in the db
00224     if (db.next())
00225     {
00226         info.id            = db.value(0).toInt();
00227         info.updateTimeout = db.value(2).toUInt() * 1000;
00228         info.scriptTimeout = db.value(3).toUInt();
00229 
00230         // compare versions, if equal... be happy
00231         QString dbver = db.value(6).toString();
00232         if (dbver == info.version)
00233         {
00234             info.types = db.value(8).toString().split(",");
00235         }
00236         else
00237         {
00238             // versions differ, change db to match script output
00239             LOG(VB_GENERAL, LOG_INFO, "New version of " + info.name + " found");
00240             query = "UPDATE weathersourcesettings SET source_name = :NAME, "
00241                 "path = :PATH, author = :AUTHOR, version = :VERSION, "
00242                 "email = :EMAIL, types = :TYPES WHERE sourceid = :ID";
00243             db.prepare(query);
00244             // these info values were populated when getting the version number
00245             // we leave the timeout values in
00246             db.bindValue(":NAME", info.name);
00247             db.bindValue(":PATH", info.program);
00248             db.bindValue(":AUTHOR", info.author);
00249             db.bindValue(":VERSION", info.version);
00250 
00251             // run the script to get supported data types
00252             info.types = WeatherSource::ProbeTypes(info.path, info.program);
00253 
00254             db.bindValue(":TYPES", info.types.join(","));
00255             db.bindValue(":ID", info.id);
00256             db.bindValue(":EMAIL", info.email);
00257             if (!db.exec())
00258             {
00259                 MythDB::DBError("Updating weather source settings.", db);
00260                 return NULL;
00261             }
00262         }
00263     }
00264     else
00265     {
00266         // Script is not in db, probe it and insert it into db
00267         query = "INSERT INTO weathersourcesettings "
00268                 "(hostname, source_name, update_timeout, retrieve_timeout, "
00269                 "path, author, version, email, types) "
00270                 "VALUES (:HOST, :NAME, :UPDATETO, :RETTO, :PATH, :AUTHOR, "
00271                 ":VERSION, :EMAIL, :TYPES);";
00272 
00273         if (!WeatherSource::ProbeTimeouts(info.path,
00274                                           info.program,
00275                                           info.updateTimeout,
00276                                           info.scriptTimeout))
00277         {
00278             return NULL;
00279         }
00280         db.prepare(query);
00281         db.bindValue(":NAME", info.name);
00282         db.bindValue(":HOST", gCoreContext->GetHostName());
00283         db.bindValue(":UPDATETO", QString::number(info.updateTimeout/1000));
00284         db.bindValue(":RETTO", QString::number(info.scriptTimeout));
00285         db.bindValue(":PATH", info.program);
00286         db.bindValue(":AUTHOR", info.author);
00287         db.bindValue(":VERSION", info.version);
00288         db.bindValue(":EMAIL", info.email);
00289         info.types = ProbeTypes(info.path, info.program);
00290         db.bindValue(":TYPES", info.types.join(","));
00291         if (!db.exec())
00292         {
00293             MythDB::DBError("Inserting weather source", db);
00294             return NULL;
00295         }
00296         query = "SELECT sourceid FROM weathersourcesettings "
00297                 "WHERE source_name = :NAME AND hostname = :HOST;";
00298         // a little annoying, but look at what we just inserted to get the id
00299         // number, not sure if we really need it, but better safe than sorry.
00300         db.prepare(query);
00301         db.bindValue(":HOST", gCoreContext->GetHostName());
00302         db.bindValue(":NAME", info.name);
00303         if (!db.exec())
00304         {
00305             MythDB::DBError("Getting weather sourceid", db);
00306             return NULL;
00307         }
00308         else if (!db.next())
00309         {
00310             LOG(VB_GENERAL, LOG_ERR, "Error getting weather sourceid");
00311             return NULL;
00312         }
00313         else
00314         {
00315             info.id = db.value(0).toInt();
00316         }
00317     }
00318 
00319     return new ScriptInfo(info);
00320 }
00321 
00322 /*
00323  * Watch out, we store the parameter as a member variable, don't go deleting it,
00324  * that wouldn't be good.
00325  */
00326 WeatherSource::WeatherSource(ScriptInfo *info)
00327     : m_ready(info ? true : false),    m_inuse(info ? true : false),
00328       m_info(info),
00329       m_ms(NULL),
00330       m_locale(""),
00331       m_cachefile(""),
00332       m_units(SI_UNITS),
00333       m_updateTimer(new QTimer(this)), m_connectCnt(0)
00334 {
00335     QDir dir(GetConfDir());
00336     if (!dir.exists("MythWeather"))
00337         dir.mkdir("MythWeather");
00338     dir.cd("MythWeather");
00339     if (!dir.exists(info->name))
00340         dir.mkdir(info->name);
00341     dir.cd(info->name);
00342     m_dir = dir.absolutePath();
00343 
00344     connect( m_updateTimer, SIGNAL(timeout()), this, SLOT(updateTimeout()));
00345 }
00346 
00347 WeatherSource::~WeatherSource()
00348 {
00349     if (m_ms)
00350     {
00351         m_ms->Kill();
00352         delete m_ms;
00353     }
00354     delete m_updateTimer;
00355 }
00356 
00357 void WeatherSource::connectScreen(WeatherScreen *ws)
00358 {
00359     connect(this, SIGNAL(newData(QString, units_t, DataMap)),
00360             ws, SLOT(newData(QString, units_t, DataMap)));
00361     ++m_connectCnt;
00362 
00363     if (m_data.size() > 0)
00364     {
00365         emit newData(m_locale, m_units, m_data);
00366     }
00367 }
00368 
00369 void WeatherSource::disconnectScreen(WeatherScreen *ws)
00370 {
00371     disconnect(this, 0, ws, 0);
00372     --m_connectCnt;
00373 }
00374 
00375 QStringList WeatherSource::getLocationList(const QString &str)
00376 {
00377     QString program = m_info->program;
00378     QStringList args;
00379     args << "-l";
00380     args << str;
00381 
00382     const QString loc = QString("WeatherSource::getLocationList(%1 %2): ")
00383         .arg(program).arg(args.join(" "));
00384 
00385     uint flags = kMSRunShell | kMSStdOut | kMSBuffered | 
00386                  kMSDontDisableDrawing | kMSDontBlockInputDevs;
00387     MythSystem ms(program, args, flags);
00388     ms.SetDirectory(m_info->path);
00389     ms.Run();
00390     
00391     if (ms.Wait() != GENERIC_EXIT_OK)
00392     {
00393         LOG(VB_GENERAL, LOG_ERR, loc + "Cannot run script");
00394         return QStringList();
00395     }
00396 
00397     QStringList locs;
00398     QByteArray result = ms.ReadAll();
00399     QTextStream text(result);
00400                                                                                 
00401     QTextCodec *codec = QTextCodec::codecForName("UTF-8");                     
00402     while (!text.atEnd())
00403     {
00404         QString tmp = text.readLine();
00405 
00406         while (tmp.endsWith('\n') || tmp.endsWith('\r'))
00407             tmp.chop(1);
00408 
00409         if (!tmp.isEmpty())
00410          {                                                                      
00411              QString loc_string = codec->toUnicode(tmp.toUtf8());
00412              locs << loc_string;
00413          }
00414     }
00415 
00416     return locs;
00417 }
00418 
00419 void WeatherSource::startUpdate(bool forceUpdate)
00420 {
00421     m_buffer.clear();
00422 
00423     MSqlQuery db(MSqlQuery::InitCon());
00424     LOG(VB_GENERAL, LOG_INFO, "Starting update of " + m_info->name);
00425 
00426     if (m_ms)
00427     {
00428         LOG(VB_GENERAL, LOG_ERR, QString("%1 process exists, skipping.")
00429             .arg(m_info->name));
00430         return;
00431     }
00432 
00433     if (!forceUpdate)
00434     {
00435         db.prepare("SELECT updated FROM weathersourcesettings "
00436                 "WHERE sourceid = :ID AND "
00437                 "TIMESTAMPADD(SECOND,update_timeout-15,updated) > NOW()");
00438         db.bindValue(":ID", getId());
00439         if (db.exec() && db.size() > 0)
00440         {
00441             LOG(VB_GENERAL, LOG_ERR, QString("%1 recently updated, skipping.")
00442                                         .arg(m_info->name));
00443 
00444             if (m_cachefile.isEmpty())
00445             {
00446                 QString locale_file(m_locale);
00447                 locale_file.replace("/", "-");
00448                 m_cachefile = QString("%1/cache_%2").arg(m_dir).arg(locale_file);
00449             }
00450             QFile cache(m_cachefile);
00451             if (cache.exists() && cache.open( QIODevice::ReadOnly ))
00452             {
00453                 m_buffer = cache.readAll();
00454                 cache.close();
00455 
00456                 processData();
00457 
00458                 if (m_connectCnt)
00459                 {
00460                     emit newData(m_locale, m_units, m_data);
00461                 }
00462                 return;
00463             }
00464             else
00465             {
00466                 LOG(VB_GENERAL, LOG_WARNING,
00467                     QString("No cachefile for %1, forcing update.")
00468                         .arg(m_info->name));
00469             }
00470         }
00471     }
00472 
00473     m_data.clear();
00474     QString program = "nice";
00475     QStringList args;
00476     args << m_info->program;
00477     args << "-u";
00478     args << (m_units == SI_UNITS ? "SI" : "ENG");
00479 
00480     if (!m_dir.isEmpty())
00481     {
00482         args << "-d";
00483         args << m_dir;
00484     }
00485     args << m_locale;
00486 
00487     uint flags = kMSRunShell | kMSStdOut | kMSBuffered | kMSRunBackground |
00488                  kMSDontDisableDrawing | kMSDontBlockInputDevs;
00489     m_ms = new MythSystem(program, args, flags);
00490     m_ms->SetDirectory(m_info->path);
00491 
00492     connect(m_ms, SIGNAL(finished()),  this, SLOT(processExit()));
00493     connect(m_ms, SIGNAL(error(uint)), this, SLOT(processExit(uint)));
00494 
00495     m_ms->Run(m_info->scriptTimeout);
00496 }
00497 
00498 void WeatherSource::updateTimeout()
00499 {
00500     startUpdate();
00501     startUpdateTimer();
00502 }
00503 
00504 void WeatherSource::processExit(uint status)
00505 {
00506     m_ms->disconnect(); // disconnects all signals
00507 
00508     if (status == GENERIC_EXIT_OK)
00509     {
00510         m_buffer = m_ms->ReadAll();
00511     }
00512 
00513     delete m_ms;
00514     m_ms = NULL;
00515 
00516     if (status != GENERIC_EXIT_OK)
00517     {
00518         LOG(VB_GENERAL, LOG_ERR, QString("script exit status %1").arg(status));
00519         return;
00520     }
00521 
00522     if (m_buffer.isEmpty())
00523     {
00524         LOG(VB_GENERAL, LOG_ERR, "Script returned no data");
00525         return;
00526     }
00527 
00528     if (m_cachefile.isEmpty())
00529     {
00530         QString locale_file(m_locale);
00531         locale_file.replace("/", "-");
00532         m_cachefile = QString("%1/cache_%2").arg(m_dir).arg(locale_file);
00533     }
00534     QFile cache(m_cachefile);
00535     if (cache.open( QIODevice::WriteOnly ))
00536     {
00537         cache.write(m_buffer);
00538         cache.close();
00539     }
00540     else
00541     {
00542         LOG(VB_GENERAL, LOG_ERR, QString("Unable to save data to cachefile: %1")
00543                 .arg(m_cachefile));
00544     }
00545 
00546     processData();
00547 
00548     MSqlQuery db(MSqlQuery::InitCon());
00549 
00550     db.prepare("UPDATE weathersourcesettings "
00551                "SET updated = NOW() WHERE sourceid = :ID;");
00552 
00553     db.bindValue(":ID", getId());
00554     if (!db.exec())
00555     {
00556         MythDB::DBError("Updating weather source's last update time", db);
00557         return;
00558     }
00559 
00560     if (m_connectCnt)
00561     {
00562         emit newData(m_locale, m_units, m_data);
00563     }
00564 }
00565 
00566 void WeatherSource::processData()
00567 {
00568     QTextCodec *codec = QTextCodec::codecForName("UTF-8");
00569     QString unicode_buffer = codec->toUnicode(m_buffer);
00570     QStringList data = unicode_buffer.split('\n', QString::SkipEmptyParts);
00571 
00572     m_data.clear();
00573 
00574     for (int i = 0; i < data.size(); ++i)
00575     {
00576         QStringList temp = data[i].split("::", QString::SkipEmptyParts);
00577         if (temp.size() > 2)
00578             LOG(VB_GENERAL, LOG_ERR, "Error parsing script file, ignoring");
00579         if (temp.size() < 2)
00580         {
00581             LOG(VB_GENERAL, LOG_ERR,
00582                 "Unrecoverable error parsing script output " + temp.size());
00583             LOG(VB_GENERAL, LOG_ERR, QString("data[%1]: '%2'")
00584                     .arg(i).arg(data[i]));
00585             return; // we don't emit signal
00586         }
00587 
00588         if (temp[1] != "---")
00589         {
00590             if (!m_data[temp[0]].isEmpty())
00591             {
00592                 m_data[temp[0]].append("\n" + temp[1]);
00593             }
00594             else
00595                 m_data[temp[0]] = temp[1];
00596         }
00597     }
00598 }
00599 
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends