MythTV master
weatherSource.cpp
Go to the documentation of this file.
1// C++
2#include <unistd.h>
3
4// QT headers
5#include <QApplication>
6#include <QDir>
7#include <QFile>
8#include <QTextStream>
9#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
10#include <QTextCodec>
11#else
12#include <QStringConverter>
13#endif
14
15// MythTV headers
16#include <libmythbase/compat.h>
19#include <libmythbase/mythdb.h>
23
24// MythWeather headers
25#include "weatherScreen.h"
26#include "weatherSource.h"
27
28QStringList WeatherSource::ProbeTypes(const QString& workingDirectory,
29 const QString& program)
30{
31 QStringList arguments("-t");
32 const QString loc = QString("WeatherSource::ProbeTypes(%1 %2): ")
33 .arg(program, arguments.join(" "));
34 QStringList types;
35
36 uint flags = kMSRunShell | kMSStdOut |
38 MythSystemLegacy ms(program, arguments, flags);
39 ms.SetDirectory(workingDirectory);
40 ms.Run();
41 if (ms.Wait() != GENERIC_EXIT_OK)
42 {
43 LOG(VB_GENERAL, LOG_ERR, loc + "Cannot run script");
44 return types;
45 }
46
47 QByteArray result = ms.ReadAll();
48 QTextStream text(result);
49
50 while (!text.atEnd())
51 {
52 QString tmp = text.readLine();
53
54 while (tmp.endsWith('\n') || tmp.endsWith('\r'))
55 tmp.chop(1);
56
57 if (!tmp.isEmpty())
58 types += tmp;
59 }
60
61 if (types.empty())
62 LOG(VB_GENERAL, LOG_ERR, loc + "Invalid output from -t option");
63
64 return types;
65}
66
67bool WeatherSource::ProbeTimeouts(const QString& workingDirectory,
68 const QString& program,
69 std::chrono::seconds &updateTimeout,
70 std::chrono::seconds &scriptTimeout)
71{
72 QStringList arguments("-T");
73 const QString loc = QString("WeatherSource::ProbeTimeouts(%1 %2): ")
74 .arg(program, arguments.join(" "));
75
77 scriptTimeout = DEFAULT_SCRIPT_TIMEOUT;
78
79 uint flags = kMSRunShell | kMSStdOut |
81 MythSystemLegacy ms(program, arguments, flags);
82 ms.SetDirectory(workingDirectory);
83 ms.Run();
84 if (ms.Wait() != GENERIC_EXIT_OK)
85 {
86 LOG(VB_GENERAL, LOG_ERR, loc + "Cannot run script");
87 return false;
88 }
89
90 QByteArray result = ms.ReadAll();
91 QTextStream text(result);
92
93 QStringList lines;
94 while (!text.atEnd())
95 {
96 QString tmp = text.readLine();
97
98 while (tmp.endsWith('\n') || tmp.endsWith('\r'))
99 tmp.chop(1);
100
101 if (!tmp.isEmpty())
102 lines << tmp;
103 }
104
105 if (lines.empty())
106 {
107 LOG(VB_GENERAL, LOG_ERR, loc + "Invalid Script Output! No Lines");
108 return false;
109 }
110
111 QStringList temp = lines[0].split(',');
112 if (temp.size() != 2)
113 {
114 LOG(VB_GENERAL, LOG_ERR, loc +
115 QString("Invalid Script Output! '%1'").arg(lines[0]));
116 return false;
117 }
118
119 std::array<bool,2> isOK {};
120 uint ut = temp[0].toUInt(isOK.data());
121 uint st = temp[1].toUInt(&isOK[1]);
122 if (!isOK[0] || !isOK[1])
123 {
124 LOG(VB_GENERAL, LOG_ERR, loc +
125 QString("Invalid Script Output! '%1'").arg(lines[0]));
126 return false;
127 }
128
129 updateTimeout = std::chrono::seconds(ut);
130 scriptTimeout = std::chrono::seconds(st);
131
132 return true;
133}
134
136{
137 QStringList arguments("-v");
138
139 const QString loc = QString("WeatherSource::ProbeInfo(%1 %2): ")
140 .arg(info.program, arguments.join(" "));
141
142 uint flags = kMSRunShell | kMSStdOut |
144 MythSystemLegacy ms(info.program, arguments, flags);
145 ms.SetDirectory(info.path);
146 ms.Run();
147 if (ms.Wait() != GENERIC_EXIT_OK)
148 {
149 LOG(VB_GENERAL, LOG_ERR, loc + "Cannot run script");
150 return false;
151 }
152
153 QByteArray result = ms.ReadAll();
154 QTextStream text(result);
155
156 QStringList lines;
157 while (!text.atEnd())
158 {
159 QString tmp = text.readLine();
160
161 while (tmp.endsWith('\n') || tmp.endsWith('\r'))
162 tmp.chop(1);
163
164 if (!tmp.isEmpty())
165 lines << tmp;
166 }
167
168 if (lines.empty())
169 {
170 LOG(VB_GENERAL, LOG_ERR, loc + "Invalid Script Output! No Lines");
171 return false;
172 }
173
174 QStringList temp = lines[0].split(',');
175 if (temp.size() != 4)
176 {
177 LOG(VB_GENERAL, LOG_ERR, loc +
178 QString("Invalid Script Output! '%1'").arg(lines[0]));
179 return false;
180 }
181
182 info.name = temp[0];
183 info.version = temp[1];
184 info.author = temp[2];
185 info.email = temp[3];
186
187 return true;
188}
189
190/* Basic logic of this behemouth...
191 * run script with -v flag, this returns among other things, the version number
192 * Search the database using the name (also returned from -v).
193 * if it exists, compare versions from -v and db
194 * if the same, populate the info struct from db, and we're done
195 * if they differ, get the rest of the needed information from the script and
196 * update the database, note, it does not overwrite the existing timeout values.
197 * if the script is not in the database, we probe it for types and default
198 * timeout values, and add it to the database
199 */
201{
202 if (!fi.isReadable() || !fi.isExecutable())
203 return nullptr;
204
206 info.path = fi.absolutePath();
207 info.program = fi.absoluteFilePath();
208
210 return nullptr;
211
213 QString query =
214 "SELECT sourceid, source_name, update_timeout, retrieve_timeout, "
215 "path, author, version, email, types FROM weathersourcesettings "
216 "WHERE hostname = :HOST AND source_name = :NAME;";
217 db.prepare(query);
218 db.bindValue(":HOST", gCoreContext->GetHostName());
219 db.bindValue(":NAME", info.name);
220
221 if (!db.exec())
222 {
223 LOG(VB_GENERAL, LOG_ERR, "Invalid response from database");
224 return nullptr;
225 }
226
227 // the script exists in the db
228 if (db.next())
229 {
230 info.id = db.value(0).toInt();
231 info.updateTimeout = std::chrono::seconds(db.value(2).toUInt());
232 info.scriptTimeout = std::chrono::seconds(db.value(3).toUInt());
233
234 // compare versions, if equal... be happy
235 QString dbver = db.value(6).toString();
236 if (dbver == info.version)
237 {
238 info.types = db.value(8).toString().split(",");
239 }
240 else
241 {
242 // versions differ, change db to match script output
243 LOG(VB_GENERAL, LOG_INFO, "New version of " + info.name + " found");
244 query = "UPDATE weathersourcesettings SET source_name = :NAME, "
245 "path = :PATH, author = :AUTHOR, version = :VERSION, "
246 "email = :EMAIL, types = :TYPES WHERE sourceid = :ID";
247 db.prepare(query);
248 // these info values were populated when getting the version number
249 // we leave the timeout values in
250 db.bindValue(":NAME", info.name);
251 db.bindValue(":PATH", info.program);
252 db.bindValue(":AUTHOR", info.author);
253 db.bindValue(":VERSION", info.version);
254
255 // run the script to get supported data types
256 info.types = WeatherSource::ProbeTypes(info.path, info.program);
257
258 db.bindValue(":TYPES", info.types.join(","));
259 db.bindValue(":ID", info.id);
260 db.bindValue(":EMAIL", info.email);
261 if (!db.exec())
262 {
263 MythDB::DBError("Updating weather source settings.", db);
264 return nullptr;
265 }
266 }
267 }
268 else
269 {
270 // Script is not in db, probe it and insert it into db
271 query = "INSERT INTO weathersourcesettings "
272 "(hostname, source_name, update_timeout, retrieve_timeout, "
273 "path, author, version, email, types) "
274 "VALUES (:HOST, :NAME, :UPDATETO, :RETTO, :PATH, :AUTHOR, "
275 ":VERSION, :EMAIL, :TYPES);";
276
278 info.program,
279 info.updateTimeout,
280 info.scriptTimeout))
281 {
282 return nullptr;
283 }
284 db.prepare(query);
285 db.bindValue(":NAME", info.name);
286 db.bindValue(":HOST", gCoreContext->GetHostName());
287 db.bindValue(":UPDATETO", QString::number(info.updateTimeout.count()));
288 db.bindValue(":RETTO", QString::number(info.scriptTimeout.count()));
289 db.bindValue(":PATH", info.program);
290 db.bindValue(":AUTHOR", info.author);
291 db.bindValue(":VERSION", info.version);
292 db.bindValue(":EMAIL", info.email);
293 info.types = ProbeTypes(info.path, info.program);
294 db.bindValue(":TYPES", info.types.join(","));
295 if (!db.exec())
296 {
297 MythDB::DBError("Inserting weather source", db);
298 return nullptr;
299 }
300 query = "SELECT sourceid FROM weathersourcesettings "
301 "WHERE source_name = :NAME AND hostname = :HOST;";
302 // a little annoying, but look at what we just inserted to get the id
303 // number, not sure if we really need it, but better safe than sorry.
304 db.prepare(query);
305 db.bindValue(":HOST", gCoreContext->GetHostName());
306 db.bindValue(":NAME", info.name);
307 if (!db.exec())
308 {
309 MythDB::DBError("Getting weather sourceid", db);
310 return nullptr;
311 }
312 if (!db.next())
313 {
314 LOG(VB_GENERAL, LOG_ERR, "Error getting weather sourceid");
315 return nullptr;
316 }
317 info.id = db.value(0).toInt();
318 }
319
320 return new ScriptInfo(info);
321}
322
330 : m_ready(info != nullptr),
331 m_inuse(info != nullptr),
332 m_info(info),
333 m_updateTimer(new QTimer(this))
334{
335 QDir dir(GetConfDir());
336 if (!dir.exists("MythWeather"))
337 dir.mkdir("MythWeather");
338 dir.cd("MythWeather");
339 if (info != nullptr) {
340 if (!dir.exists(info->name))
341 dir.mkdir(info->name);
342 dir.cd(info->name);
343 }
344 m_dir = dir.absolutePath();
345
347}
348
350{
351 if (m_ms)
352 {
354 m_ms->Wait(5s);
355 delete m_ms;
356 }
357 delete m_updateTimer;
358}
359
361{
362 connect(this, &WeatherSource::newData,
364 ++m_connectCnt;
365
366 if (!m_data.empty())
367 {
369 }
370}
371
373{
374 disconnect(this, nullptr, ws, nullptr);
375 --m_connectCnt;
376}
377
378QStringList WeatherSource::getLocationList(const QString &str)
379{
380 QString program = m_info->program;
381 QStringList args;
382 args << "-l";
383 args << str;
384
385 const QString loc = QString("WeatherSource::getLocationList(%1 %2): ")
386 .arg(program, args.join(" "));
387
388 uint flags = kMSRunShell | kMSStdOut |
390 MythSystemLegacy ms(program, args, flags);
392 ms.Run();
393
394 if (ms.Wait() != GENERIC_EXIT_OK)
395 {
396 LOG(VB_GENERAL, LOG_ERR, loc + "Cannot run script");
397 return {};
398 }
399
400 QStringList locs;
401 QByteArray result = ms.ReadAll();
402 QTextStream text(result);
403#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
404 text.setCodec("UTF-8");
405#else
406 text.setEncoding(QStringConverter::Utf8);
407#endif
408 while (!text.atEnd())
409 {
410 QString tmp = text.readLine().trimmed();
411 if (!tmp.isEmpty())
412 locs << tmp;
413 }
414
415 return locs;
416}
417
418void WeatherSource::startUpdate(bool forceUpdate)
419{
420 m_buffer.clear();
421
423 LOG(VB_GENERAL, LOG_INFO, "Starting update of " + m_info->name);
424
425 if (m_ms)
426 {
427 LOG(VB_GENERAL, LOG_ERR, QString("%1 process exists, skipping.")
428 .arg(m_info->name));
429 return;
430 }
431
432 if (!forceUpdate)
433 {
434 db.prepare("SELECT updated FROM weathersourcesettings "
435 "WHERE sourceid = :ID AND "
436 "TIMESTAMPADD(SECOND,update_timeout-15,updated) > NOW()");
437 db.bindValue(":ID", getId());
438 if (db.exec() && db.size() > 0)
439 {
440 LOG(VB_GENERAL, LOG_NOTICE, QString("%1 recently updated, skipping.")
441 .arg(m_info->name));
442
443 if (m_cachefile.isEmpty())
444 {
445 QString locale_file(m_locale);
446 locale_file.replace("/", "-");
447 m_cachefile = QString("%1/cache_%2").arg(m_dir, locale_file);
448 }
449 QFile cache(m_cachefile);
450 if (cache.exists() && cache.open( QIODevice::ReadOnly ))
451 {
452 m_buffer = cache.readAll();
453 cache.close();
454
455 processData();
456
457 if (m_connectCnt)
458 {
460 }
461 return;
462 }
463 LOG(VB_GENERAL, LOG_NOTICE,
464 QString("No cachefile for %1, forcing update.")
465 .arg(m_info->name));
466 }
467 }
468
469 m_data.clear();
470 QString program = "nice";
471 QStringList args;
472 args << m_info->program;
473 args << "-u";
474 args << (m_units == SI_UNITS ? "SI" : "ENG");
475
476 if (!m_dir.isEmpty())
477 {
478 args << "-d";
479 args << m_dir;
480 }
481 args << m_locale;
482
485 m_ms = new MythSystemLegacy(program, args, flags);
487
489 this, qOverload<>(&WeatherSource::processExit));
491 this, qOverload<uint>(&WeatherSource::processExit));
492
494}
495
497{
498 startUpdate();
500}
501
503{
504 m_ms->disconnect(); // disconnects all signals
505
506 if (status == GENERIC_EXIT_OK)
507 {
508 m_buffer = m_ms->ReadAll();
509 }
510
511 delete m_ms;
512 m_ms = nullptr;
513
514 if (status != GENERIC_EXIT_OK)
515 {
516 LOG(VB_GENERAL, LOG_ERR, QString("script exit status %1").arg(status));
517 return;
518 }
519
520 if (m_buffer.isEmpty())
521 {
522 LOG(VB_GENERAL, LOG_ERR, "Script returned no data");
523 return;
524 }
525
526 if (m_cachefile.isEmpty())
527 {
528 QString locale_file(m_locale);
529 locale_file.replace("/", "-");
530 m_cachefile = QString("%1/cache_%2").arg(m_dir, locale_file);
531 }
532 QFile cache(m_cachefile);
533 if (cache.open( QIODevice::WriteOnly ))
534 {
535 cache.write(m_buffer);
536 cache.close();
537 }
538 else
539 {
540 LOG(VB_GENERAL, LOG_ERR, QString("Unable to save data to cachefile: %1")
541 .arg(m_cachefile));
542 }
543
544 processData();
545
547
548 db.prepare("UPDATE weathersourcesettings "
549 "SET updated = NOW() WHERE sourceid = :ID;");
550
551 db.bindValue(":ID", getId());
552 if (!db.exec())
553 {
554 MythDB::DBError("Updating weather source's last update time", db);
555 return;
556 }
557
558 if (m_connectCnt)
559 {
561 }
562}
563
565{
567}
568
570{
571 QString unicode_buffer = QString::fromUtf8(m_buffer);
572 QStringList data = unicode_buffer.split('\n', Qt::SkipEmptyParts);
573
574 m_data.clear();
575
576 for (int i = 0; i < data.size(); ++i)
577 {
578 QStringList temp = data[i].split("::", Qt::SkipEmptyParts);
579 if (temp.size() > 2)
580 LOG(VB_GENERAL, LOG_ERR, "Error parsing script file, ignoring");
581 if (temp.size() < 2)
582 {
583 LOG(VB_GENERAL, LOG_ERR,
584 QString("Unrecoverable error parsing script output %1")
585 .arg(temp.size()));
586 LOG(VB_GENERAL, LOG_ERR, QString("data[%1]: '%2'")
587 .arg(i).arg(data[i]));
588 return; // we don't emit signal
589 }
590
591 if (temp[1] != "---")
592 {
593 if (!m_data[temp[0]].isEmpty())
594 {
595 m_data[temp[0]].append("\n" + temp[1]);
596 }
597 else
598 {
599 m_data[temp[0]] = temp[1];
600 }
601 }
602 }
603}
604
QSqlQuery wrapper that fetches a DB connection from the connection pool.
Definition: mythdbcon.h:128
bool prepare(const QString &query)
QSqlQuery::prepare() is not thread safe in Qt <= 3.3.2.
Definition: mythdbcon.cpp:837
QVariant value(int i) const
Definition: mythdbcon.h:204
int size(void) const
Definition: mythdbcon.h:214
bool exec(void)
Wrap QSqlQuery::exec() so we can display SQL.
Definition: mythdbcon.cpp:618
void bindValue(const QString &placeholder, const QVariant &val)
Add a single binding.
Definition: mythdbcon.cpp:888
bool next(void)
Wrap QSqlQuery::next() so we can display the query results.
Definition: mythdbcon.cpp:812
static MSqlQueryInfo InitCon(ConnectionReuse _reuse=kNormalConnection)
Only use this in combination with MSqlQuery constructor.
Definition: mythdbcon.cpp:550
QString GetHostName(void)
static void DBError(const QString &where, const MSqlQuery &query)
Definition: mythdb.cpp:226
uint Wait(std::chrono::seconds timeout=0s)
void error(uint status)
void finished(void)
void SetDirectory(const QString &directory)
void Run(std::chrono::seconds timeout=0s)
Runs a command inside the /bin/sh shell. Returns immediately.
void Signal(MythSignal sig)
QByteArray & ReadAll()
std::chrono::seconds scriptTimeout
Definition: weatherSource.h:31
QString name
Definition: weatherSource.h:24
QString path
Definition: weatherSource.h:30
QString program
Definition: weatherSource.h:29
Weather screen.
Definition: weatherScreen.h:27
virtual void newData(const QString &, units_t, DataMap data)
static bool ProbeTimeouts(const QString &workingDirectory, const QString &program, std::chrono::seconds &updateTimeout, std::chrono::seconds &scriptTimeout)
WeatherSource(ScriptInfo *info)
Watch out, we store the parameter as a member variable, don't go deleting it, that wouldn't be good.
void startUpdate(bool forceUpdate=false)
static ScriptInfo * ProbeScript(const QFileInfo &fi)
static bool ProbeInfo(ScriptInfo &scriptInfo)
QString m_locale
Definition: weatherSource.h:99
~WeatherSource() override
QTimer * m_updateTimer
QStringList getLocationList(const QString &str)
void disconnectScreen(WeatherScreen *ws)
QByteArray m_buffer
static QStringList ProbeTypes(const QString &workingDirectory, const QString &program)
QString m_cachefile
ScriptInfo * m_info
Definition: weatherSource.h:96
void startUpdateTimer()
Definition: weatherSource.h:72
void newData(QString, units_t, DataMap)
MythSystemLegacy * m_ms
Definition: weatherSource.h:97
void connectScreen(WeatherScreen *ws)
@ GENERIC_EXIT_OK
Exited with no error.
Definition: exitcodes.h:13
unsigned int uint
Definition: freesurround.h:24
static guint32 * tmp
Definition: goom_core.cpp:26
static const struct wl_interface * types[]
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
QString GetConfDir(void)
Definition: mythdirs.cpp:263
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
@ kSignalKill
Definition: mythsystem.h:64
@ kMSDontBlockInputDevs
avoid blocking LIRC & Joystick Menu
Definition: mythsystem.h:36
@ kMSStdOut
allow access to stdout
Definition: mythsystem.h:41
@ kMSRunShell
run process through shell
Definition: mythsystem.h:43
@ kMSRunBackground
run child in the background
Definition: mythsystem.h:38
@ kMSDontDisableDrawing
avoid disabling UI drawing
Definition: mythsystem.h:37
dictionary info
Definition: azlyrics.py:7
static constexpr std::chrono::minutes DEFAULT_UPDATE_TIMEOUT
Definition: weatherUtils.h:20
static constexpr std::chrono::seconds DEFAULT_SCRIPT_TIMEOUT
Definition: weatherUtils.h:21
static constexpr uint8_t SI_UNITS
Definition: weatherUtils.h:18