1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25 
26 #include "settingspage.h"
27 #include "updateinfoplugin.h"
28 
29 #include <coreplugin/actionmanager/actioncontainer.h>
30 #include <coreplugin/actionmanager/actionmanager.h>
31 #include <coreplugin/coreconstants.h>
32 #include <coreplugin/icore.h>
33 #include <coreplugin/settingsdatabase.h>
34 #include <coreplugin/shellcommand.h>
35 #include <utils/algorithm.h>
36 #include <utils/fileutils.h>
37 #include <utils/infobar.h>
38 #include <utils/qtcassert.h>
39 #include <utils/qtcprocess.h>
40 
41 #include <QDate>
42 #include <QDomDocument>
43 #include <QFile>
44 #include <QFileInfo>
45 #include <QLabel>
46 #include <QMenu>
47 #include <QMetaEnum>
48 #include <QPointer>
49 #include <QProcessEnvironment>
50 #include <QTimer>
51 
52 namespace {
53     static const char UpdaterGroup[] = "Updater";
54     static const char MaintenanceToolKey[] = "MaintenanceTool";
55     static const char AutomaticCheckKey[] = "AutomaticCheck";
56     static const char CheckIntervalKey[] = "CheckUpdateInterval";
57     static const char LastCheckDateKey[] = "LastCheckDate";
58     static const quint32 OneMinute = 60000;
59     static const quint32 OneHour = 3600000;
60     static const char InstallUpdates[] = "UpdateInfo.InstallUpdates";
61 }
62 
63 using namespace Core;
64 
65 namespace UpdateInfo {
66 namespace Internal {
67 
68 class UpdateInfoPluginPrivate
69 {
70 public:
71     QString m_maintenanceTool;
72     QPointer<ShellCommand> m_checkUpdatesCommand;
73     QPointer<FutureProgress> m_progress;
74     QString m_collectedOutput;
75     QTimer *m_checkUpdatesTimer = nullptr;
76 
77     struct Settings
78     {
79         bool automaticCheck = true;
80         UpdateInfoPlugin::CheckUpdateInterval checkInterval = UpdateInfoPlugin::WeeklyCheck;
81     };
82     Settings m_settings;
83     QDate m_lastCheckDate;
84 };
85 
UpdateInfoPlugin()86 UpdateInfoPlugin::UpdateInfoPlugin()
87     : d(new UpdateInfoPluginPrivate)
88 {
89     d->m_checkUpdatesTimer = new QTimer(this);
90     d->m_checkUpdatesTimer->setTimerType(Qt::VeryCoarseTimer);
91     d->m_checkUpdatesTimer->setInterval(OneHour);
92     connect(d->m_checkUpdatesTimer, &QTimer::timeout,
93             this, &UpdateInfoPlugin::doAutoCheckForUpdates);
94 }
95 
~UpdateInfoPlugin()96 UpdateInfoPlugin::~UpdateInfoPlugin()
97 {
98     stopCheckForUpdates();
99     if (!d->m_maintenanceTool.isEmpty())
100         saveSettings();
101 
102     delete d;
103 }
104 
startAutoCheckForUpdates()105 void UpdateInfoPlugin::startAutoCheckForUpdates()
106 {
107     doAutoCheckForUpdates();
108 
109     d->m_checkUpdatesTimer->start();
110 }
111 
stopAutoCheckForUpdates()112 void UpdateInfoPlugin::stopAutoCheckForUpdates()
113 {
114     d->m_checkUpdatesTimer->stop();
115 }
116 
doAutoCheckForUpdates()117 void UpdateInfoPlugin::doAutoCheckForUpdates()
118 {
119     if (d->m_checkUpdatesCommand)
120         return; // update task is still running (might have been run manually just before)
121 
122     if (nextCheckDate().isValid() && nextCheckDate() > QDate::currentDate())
123         return; // not a time for check yet
124 
125     startCheckForUpdates();
126 }
127 
startCheckForUpdates()128 void UpdateInfoPlugin::startCheckForUpdates()
129 {
130     stopCheckForUpdates();
131 
132     Utils::Environment env = Utils::Environment::systemEnvironment();
133     env.set("QT_LOGGING_RULES", "*=false");
134     d->m_checkUpdatesCommand = new ShellCommand(QString(), env);
135     d->m_checkUpdatesCommand->setDisplayName(tr("Checking for Updates"));
136     connect(d->m_checkUpdatesCommand, &ShellCommand::stdOutText, this, &UpdateInfoPlugin::collectCheckForUpdatesOutput);
137     connect(d->m_checkUpdatesCommand, &ShellCommand::finished, this, &UpdateInfoPlugin::checkForUpdatesFinished);
138     d->m_checkUpdatesCommand->addJob({Utils::FilePath::fromString(d->m_maintenanceTool), {"--checkupdates"}},
139                                      60 * 3, // 3 minutes timeout
140                                      /*workingDirectory=*/QString(),
141                                      [](int /*exitCode*/) { return Utils::QtcProcess::FinishedWithSuccess; });
142     d->m_checkUpdatesCommand->execute();
143     d->m_progress = d->m_checkUpdatesCommand->futureProgress();
144     if (d->m_progress) {
145         d->m_progress->setKeepOnFinish(FutureProgress::KeepOnFinishTillUserInteraction);
146         d->m_progress->setSubtitleVisibleInStatusBar(true);
147     }
148     emit checkForUpdatesRunningChanged(true);
149 }
150 
stopCheckForUpdates()151 void UpdateInfoPlugin::stopCheckForUpdates()
152 {
153     if (!d->m_checkUpdatesCommand)
154         return;
155 
156     d->m_collectedOutput.clear();
157     d->m_checkUpdatesCommand->disconnect();
158     d->m_checkUpdatesCommand->cancel();
159     d->m_checkUpdatesCommand = nullptr;
160     emit checkForUpdatesRunningChanged(false);
161 }
162 
collectCheckForUpdatesOutput(const QString & contents)163 void UpdateInfoPlugin::collectCheckForUpdatesOutput(const QString &contents)
164 {
165     d->m_collectedOutput += contents;
166 }
167 
168 struct Update
169 {
170     QString name;
171     QString version;
172 };
173 
availableUpdates(const QDomDocument & document)174 static QList<Update> availableUpdates(const QDomDocument &document)
175 {
176     if (document.isNull() || !document.firstChildElement().hasChildNodes())
177         return {};
178     QList<Update> result;
179     const QDomNodeList updates = document.firstChildElement().elementsByTagName("update");
180     for (int i = 0; i < updates.size(); ++i) {
181         const QDomNode node = updates.item(i);
182         if (node.isElement()) {
183             const QDomElement element = node.toElement();
184             if (element.hasAttribute("name"))
185                 result.append({element.attribute("name"), element.attribute("version")});
186         }
187     }
188     return result;
189 }
190 
checkForUpdatesFinished()191 void UpdateInfoPlugin::checkForUpdatesFinished()
192 {
193     setLastCheckDate(QDate::currentDate());
194 
195     QDomDocument document;
196     document.setContent(d->m_collectedOutput);
197 
198     stopCheckForUpdates();
199 
200     if (!document.isNull() && document.firstChildElement().hasChildNodes()) {
201         // progress details are shown until user interaction for the "no updates" case,
202         // so we can show the "No updates found" text, but if we have updates we don't
203         // want to keep it around
204         if (d->m_progress)
205             d->m_progress->setKeepOnFinish(FutureProgress::HideOnFinish);
206         emit newUpdatesAvailable(true);
207         Utils::InfoBarEntry info(InstallUpdates, tr("New updates are available. Start the update?"));
208         info.setCustomButtonInfo(tr("Start Update"), [this] {
209             Core::ICore::infoBar()->removeInfo(InstallUpdates);
210             startUpdater();
211         });
212         const QList<Update> updates = availableUpdates(document);
213         info.setDetailsWidgetCreator([updates]() -> QWidget * {
214             const QString updateText = Utils::transform(updates, [](const Update &u) {
215                                            return u.version.isEmpty()
216                                                       ? u.name
217                                                       : tr("%1 (%2)", "Package name and version")
218                                                             .arg(u.name, u.version);
219                                        }).join("</li><li>");
220             auto label = new QLabel;
221             label->setText("<qt><p>" + tr("Available updates:") + "<ul><li>" + updateText
222                            + "</li></ul></p></qt>");
223             label->setContentsMargins(0, 0, 0, 8);
224             return label;
225         });
226         Core::ICore::infoBar()->removeInfo(InstallUpdates); // remove any existing notifications
227         Core::ICore::infoBar()->unsuppressInfo(InstallUpdates);
228         Core::ICore::infoBar()->addInfo(info);
229     } else {
230         emit newUpdatesAvailable(false);
231         if (d->m_progress)
232             d->m_progress->setSubtitle(tr("No updates found."));
233     }
234 }
235 
isCheckForUpdatesRunning() const236 bool UpdateInfoPlugin::isCheckForUpdatesRunning() const
237 {
238     return d->m_checkUpdatesCommand;
239 }
240 
extensionsInitialized()241 void UpdateInfoPlugin::extensionsInitialized()
242 {
243     if (isAutomaticCheck())
244         QTimer::singleShot(OneMinute, this, &UpdateInfoPlugin::startAutoCheckForUpdates);
245 }
246 
initialize(const QStringList &,QString * errorMessage)247 bool UpdateInfoPlugin::initialize(const QStringList & /* arguments */, QString *errorMessage)
248 {
249     loadSettings();
250 
251     if (d->m_maintenanceTool.isEmpty()) {
252         *errorMessage = tr("Could not determine location of maintenance tool. Please check "
253             "your installation if you did not enable this plugin manually.");
254         return false;
255     }
256 
257     if (!QFileInfo(d->m_maintenanceTool).isExecutable()) {
258         *errorMessage = tr("The maintenance tool at \"%1\" is not an executable. Check your installation.")
259             .arg(d->m_maintenanceTool);
260         d->m_maintenanceTool.clear();
261         return false;
262     }
263 
264     connect(ICore::instance(), &ICore::saveSettingsRequested,
265             this, &UpdateInfoPlugin::saveSettings);
266 
267     (void) new SettingsPage(this);
268 
269     QAction *checkForUpdatesAction = new QAction(tr("Check for Updates"), this);
270     checkForUpdatesAction->setMenuRole(QAction::ApplicationSpecificRole);
271     Core::Command *checkForUpdatesCommand = Core::ActionManager::registerAction(checkForUpdatesAction, "Updates.CheckForUpdates");
272     connect(checkForUpdatesAction, &QAction::triggered, this, &UpdateInfoPlugin::startCheckForUpdates);
273     ActionContainer *const helpContainer = ActionManager::actionContainer(Core::Constants::M_HELP);
274     helpContainer->addAction(checkForUpdatesCommand, Constants::G_HELP_UPDATES);
275 
276     return true;
277 }
278 
loadSettings() const279 void UpdateInfoPlugin::loadSettings() const
280 {
281     UpdateInfoPluginPrivate::Settings def;
282     QSettings *settings = ICore::settings();
283     const QString updaterKey = QLatin1String(UpdaterGroup) + '/';
284     d->m_maintenanceTool = settings->value(updaterKey + MaintenanceToolKey).toString();
285     d->m_lastCheckDate = settings->value(updaterKey + LastCheckDateKey, QDate()).toDate();
286     d->m_settings.automaticCheck
287         = settings->value(updaterKey + AutomaticCheckKey, def.automaticCheck).toBool();
288     const QMetaObject *mo = metaObject();
289     const QMetaEnum me = mo->enumerator(mo->indexOfEnumerator(CheckIntervalKey));
290     if (QTC_GUARD(me.isValid())) {
291         const QString checkInterval = settings
292                                           ->value(updaterKey + CheckIntervalKey,
293                                                   me.valueToKey(def.checkInterval))
294                                           .toString();
295         bool ok = false;
296         const int newValue = me.keyToValue(checkInterval.toUtf8(), &ok);
297         if (ok)
298             d->m_settings.checkInterval = static_cast<CheckUpdateInterval>(newValue);
299     }
300 }
301 
saveSettings()302 void UpdateInfoPlugin::saveSettings()
303 {
304     UpdateInfoPluginPrivate::Settings def;
305     Utils::QtcSettings *settings = ICore::settings();
306     settings->beginGroup(UpdaterGroup);
307     settings->setValueWithDefault(LastCheckDateKey, d->m_lastCheckDate, QDate());
308     settings->setValueWithDefault(AutomaticCheckKey,
309                                   d->m_settings.automaticCheck,
310                                   def.automaticCheck);
311     // Note: don't save MaintenanceToolKey on purpose! This setting may be set only by installer.
312     // If creator is run not from installed SDK, the setting can be manually created here:
313     // [CREATOR_INSTALLATION_LOCATION]/share/qtcreator/QtProject/QtCreator.ini or
314     // [CREATOR_INSTALLATION_LOCATION]/Qt Creator.app/Contents/Resources/QtProject/QtCreator.ini on OS X
315     const QMetaObject *mo = metaObject();
316     const QMetaEnum me = mo->enumerator(mo->indexOfEnumerator(CheckIntervalKey));
317     settings->setValueWithDefault(CheckIntervalKey,
318                                   QString::fromUtf8(me.valueToKey(d->m_settings.checkInterval)),
319                                   QString::fromUtf8(me.valueToKey(def.checkInterval)));
320     settings->endGroup();
321 }
322 
isAutomaticCheck() const323 bool UpdateInfoPlugin::isAutomaticCheck() const
324 {
325     return d->m_settings.automaticCheck;
326 }
327 
setAutomaticCheck(bool on)328 void UpdateInfoPlugin::setAutomaticCheck(bool on)
329 {
330     if (d->m_settings.automaticCheck == on)
331         return;
332 
333     d->m_settings.automaticCheck = on;
334     if (on)
335         startAutoCheckForUpdates();
336     else
337         stopAutoCheckForUpdates();
338 }
339 
checkUpdateInterval() const340 UpdateInfoPlugin::CheckUpdateInterval UpdateInfoPlugin::checkUpdateInterval() const
341 {
342     return d->m_settings.checkInterval;
343 }
344 
setCheckUpdateInterval(UpdateInfoPlugin::CheckUpdateInterval interval)345 void UpdateInfoPlugin::setCheckUpdateInterval(UpdateInfoPlugin::CheckUpdateInterval interval)
346 {
347     if (d->m_settings.checkInterval == interval)
348         return;
349 
350     d->m_settings.checkInterval = interval;
351 }
352 
lastCheckDate() const353 QDate UpdateInfoPlugin::lastCheckDate() const
354 {
355     return d->m_lastCheckDate;
356 }
357 
setLastCheckDate(const QDate & date)358 void UpdateInfoPlugin::setLastCheckDate(const QDate &date)
359 {
360     if (d->m_lastCheckDate == date)
361         return;
362 
363     d->m_lastCheckDate = date;
364     emit lastCheckDateChanged(date);
365 }
366 
nextCheckDate() const367 QDate UpdateInfoPlugin::nextCheckDate() const
368 {
369     return nextCheckDate(d->m_settings.checkInterval);
370 }
371 
nextCheckDate(CheckUpdateInterval interval) const372 QDate UpdateInfoPlugin::nextCheckDate(CheckUpdateInterval interval) const
373 {
374     if (!d->m_lastCheckDate.isValid())
375         return QDate();
376 
377     if (interval == DailyCheck)
378         return d->m_lastCheckDate.addDays(1);
379     if (interval == WeeklyCheck)
380         return d->m_lastCheckDate.addDays(7);
381     return d->m_lastCheckDate.addMonths(1);
382 }
383 
startUpdater()384 void UpdateInfoPlugin::startUpdater()
385 {
386     QProcess::startDetached(d->m_maintenanceTool, QStringList(QLatin1String("--updater")));
387 }
388 
389 } //namespace Internal
390 } //namespace UpdateInfo
391