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