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