Branch data Line data Source code
1 : : // Copyright (C) 2021 The Qt Company Ltd.
2 : : // Copyright (C) 2019 Luxoft Sweden AB
3 : : // Copyright (C) 2018 Pelagicore AG
4 : : // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
5 : :
6 : : #include <QTemporaryDir>
7 : : #include <QMessageAuthenticationCode>
8 : : #include <QPointer>
9 : :
10 : : #include "logging.h"
11 : : #include "packagemanager_p.h"
12 : : #include "package.h"
13 : : #include "packageinfo.h"
14 : : #include "packageextractor.h"
15 : : #include "application.h"
16 : : #include "applicationinfo.h"
17 : : #include "exception.h"
18 : : #include "packagemanager.h"
19 : : #include "utilities.h"
20 : : #include "signature.h"
21 : : #include "installationtask.h"
22 : :
23 : : #include <memory>
24 : : #ifdef Q_OS_UNIX
25 : : # include <unistd.h>
26 : : #endif
27 : :
28 : : using namespace Qt::StringLiterals;
29 : :
30 : :
31 : : /*
32 : : Overview of what happens on an installation of an app with <id> to <location>:
33 : :
34 : : Step 1 -- startInstallation()
35 : : =============================
36 : :
37 : : delete <location>/<id>+
38 : :
39 : : create dir <location>/<id>+
40 : : set <extractiondir> to <location>/<id>+
41 : :
42 : :
43 : : Step 2 -- unpack files
44 : : ======================
45 : :
46 : : PackageExtractor does its job
47 : :
48 : :
49 : : Step 3 -- finishInstallation()
50 : : ================================
51 : :
52 : : if (exists <location>/<id>)
53 : : set <isupdate> to <true>
54 : :
55 : : create installation report at <extractiondir>/.installation-report.yaml
56 : :
57 : : if (not <isupdate>)
58 : : create document directory
59 : :
60 : : if (optional uid separation)
61 : : chown/chmod recursively in <extractiondir> and document directory
62 : :
63 : :
64 : : Step 3.1 -- final rename in finishInstallation()
65 : : ==================================================
66 : :
67 : : if (<isupdate>)
68 : : rename <location>/<id> to <location>/<id>-
69 : : rename <location>/<id>+ to <location>/<id>
70 : : */
71 : :
72 : : QT_BEGIN_NAMESPACE_AM
73 : :
74 : :
75 : :
76 : : // The standard QTemporaryDir destructor cannot cope with read-only sub-directories.
77 : : class TemporaryDir : public QTemporaryDir
78 : : {
79 : : public:
80 : 48 : TemporaryDir()
81 : 48 : : QTemporaryDir()
82 : 48 : { }
83 : : explicit TemporaryDir(const QString &templateName)
84 : : : QTemporaryDir(templateName)
85 : : { }
86 : 48 : ~TemporaryDir()
87 : : {
88 : 48 : recursiveOperation(path(), safeRemove);
89 : 48 : }
90 : : private:
91 : : Q_DISABLE_COPY_MOVE(TemporaryDir)
92 : : };
93 : :
94 : :
95 : : QMutex InstallationTask::s_serializeFinishInstallation { };
96 : :
97 : 49 : InstallationTask::InstallationTask(const QString &installationPath, const QString &documentPath,
98 : 49 : const QUrl &sourceUrl, QObject *parent)
99 : : : AsynchronousTask(parent)
100 : 98 : , m_pm(PackageManager::instance())
101 : 49 : , m_installationPath(installationPath)
102 : 49 : , m_documentPath(documentPath)
103 [ + - + - : 196 : , m_sourceUrl(sourceUrl)
+ - + - +
- ]
104 : : {
105 [ + - ]: 49 : setObjectName(u"QtAM-InstallationTask"_s);
106 : 49 : }
107 : :
108 : 98 : InstallationTask::~InstallationTask()
109 : 98 : { }
110 : :
111 : 8 : bool InstallationTask::cancel()
112 : : {
113 : 8 : QMutexLocker locker(&m_mutex);
114 : :
115 : : // we cannot cancel anymore after finishInstallation() has been called
116 [ + - ]: 8 : if (m_installationAcknowledged)
117 : : return false;
118 : :
119 : 8 : m_canceled = true;
120 [ + + ]: 8 : if (m_extractor)
121 [ + - ]: 7 : m_extractor->cancel();
122 [ + - ]: 8 : m_installationAcknowledgeWaitCondition.wakeAll();
123 : : return true;
124 : 8 : }
125 : :
126 : 40 : void InstallationTask::acknowledge()
127 : : {
128 : 40 : QMutexLocker locker(&m_mutex);
129 : :
130 [ - + ]: 40 : if (m_canceled)
131 [ # # ]: 0 : return;
132 : :
133 : 40 : m_installationAcknowledged = true;
134 [ + - ]: 40 : m_installationAcknowledgeWaitCondition.wakeAll();
135 : 40 : }
136 : :
137 : 48 : void InstallationTask::execute()
138 : : {
139 : 48 : try {
140 [ - + ]: 48 : if (m_installationPath.isEmpty())
141 : 0 : throw Exception("no installation location was configured");
142 : :
143 [ + - ]: 48 : TemporaryDir extractionDir;
144 [ + - - + ]: 48 : if (!extractionDir.isValid())
145 : 0 : throw Exception("could not create a temporary extraction directory");
146 : :
147 : : // protect m_canceled and changes to m_extractor
148 : 48 : QMutexLocker locker(&m_mutex);
149 [ + + ]: 48 : if (m_canceled)
150 : 1 : throw Exception(Error::Canceled, "canceled");
151 : :
152 [ + - + - : 47 : m_extractor = new PackageExtractor(m_sourceUrl, QDir(extractionDir.path()));
+ - + - ]
153 : 47 : locker.unlock();
154 : :
155 [ + - ]: 47 : connect(m_extractor, &PackageExtractor::progress, this, &AsynchronousTask::progress);
156 : :
157 [ + - + + ]: 155 : m_extractor->setFileExtractedCallback([this](const QString &f) { checkExtractedFile(f); });
158 : :
159 [ + - + + ]: 47 : if (!m_extractor->extract())
160 [ + - + - ]: 10 : throw Exception(m_extractor->errorCode(), m_extractor->errorString());
161 : :
162 [ + - - + ]: 37 : if (!m_foundInfo || !m_foundIcon)
163 : 0 : throw Exception(Error::Package, "package did not contain a valid info.yaml and icon file");
164 : :
165 [ + - ]: 37 : QByteArrayList chainOfTrust = m_pm->caCertificates();
166 : :
167 [ + - + + ]: 37 : if (!m_pm->allowInstallationOfUnsignedPackages()) {
168 [ + - + - : 15 : if (!m_extractor->installationReport().storeSignature().isEmpty()) {
+ + ]
169 : : // normal package from the store
170 [ + - + - ]: 1 : QByteArray sigDigest = m_extractor->installationReport().digest();
171 : 1 : bool sigOk = false;
172 : :
173 [ + - + - : 1 : if (Signature(sigDigest).verify(m_extractor->installationReport().storeSignature(), chainOfTrust)) {
+ - + - +
- ]
174 : : sigOk = true;
175 [ + - + - ]: 1 : } else if (!m_pm->hardwareId().isEmpty()) {
176 : : // did not verify - if we have a hardware-id, try to verify with it
177 [ + - + - ]: 2 : sigDigest = QMessageAuthenticationCode::hash(sigDigest, m_pm->hardwareId().toUtf8(), QCryptographicHash::Sha256);
178 [ + - + - : 1 : if (Signature(sigDigest).verify(m_extractor->installationReport().storeSignature(), chainOfTrust))
+ - + - -
+ ]
179 : : sigOk = true;
180 : : }
181 : : if (!sigOk)
182 : 0 : throw Exception(Error::Package, "could not verify the package's store signature");
183 [ + - + - : 15 : } else if (!m_extractor->installationReport().developerSignature().isEmpty()) {
+ + ]
184 : : // developer package - needs a device in dev mode
185 [ + - + + ]: 12 : if (!m_pm->developmentMode())
186 : 1 : throw Exception(Error::Package, "cannot install development packages on consumer devices");
187 : :
188 [ + - + - : 11 : if (!Signature(m_extractor->installationReport().digest()).verify(m_extractor->installationReport().developerSignature(), chainOfTrust))
+ - + - +
- + - +
+ ]
189 : 1 : throw Exception(Error::Package, "could not verify the package's developer signature");
190 : :
191 : : } else {
192 : 2 : throw Exception(Error::Package, "cannot install unsigned packages");
193 : : }
194 : : }
195 : :
196 [ + - ]: 33 : emit finishedPackageExtraction();
197 [ + - ]: 33 : setState(AwaitingAcknowledge);
198 : :
199 : : // now wait in a wait-condition until we get an acknowledge or we get canceled
200 : 33 : locker.relock();
201 [ + + + + ]: 54 : while (!m_canceled && !m_installationAcknowledged)
202 [ + - ]: 33 : m_installationAcknowledgeWaitCondition.wait(&m_mutex);
203 : :
204 : : // this is the last cancellation point
205 [ + + ]: 33 : if (m_canceled)
206 : 7 : throw Exception(Error::Canceled, "canceled");
207 : 26 : locker.unlock();
208 : :
209 [ + - ]: 26 : setState(Installing);
210 : :
211 : : // However many downloads are allowed to happen in parallel: we need to serialize those
212 : : // tasks here for the finishInstallation() step
213 : 26 : QMutexLocker finishLocker(&s_serializeFinishInstallation);
214 : :
215 [ + + ]: 26 : finishInstallation();
216 : :
217 : : // At this point, the installation is done, so we cannot throw anymore.
218 : :
219 : : // we need to call those PackageManager methods in the correct thread
220 : 25 : bool finishOk = false;
221 [ + - + - ]: 25 : QMetaObject::invokeMethod(PackageManager::instance(), [this, &finishOk]()
222 : 25 : { finishOk = PackageManager::instance()->finishedPackageInstall(m_packageId); },
223 : : Qt::BlockingQueuedConnection);
224 : :
225 [ - + ]: 25 : if (!finishOk)
226 [ - - - - : 1 : qCWarning(LogInstaller) << "PackageManager rejected the installation of " << m_packageId;
- - - - -
- ]
227 : :
228 [ - + ]: 84 : } catch (const Exception &e) {
229 [ + - ]: 23 : setError(e.errorCode(), e.errorString());
230 : :
231 [ + + ]: 23 : if (m_managerApproval) {
232 : : // we need to call those ApplicationManager methods in the correct thread
233 : 13 : bool cancelOk = false;
234 [ + - + - ]: 13 : QMetaObject::invokeMethod(PackageManager::instance(), [this, &cancelOk]()
235 : 13 : { cancelOk = PackageManager::instance()->canceledPackageInstall(m_packageId); },
236 : : Qt::BlockingQueuedConnection);
237 : :
238 [ - + ]: 13 : if (!cancelOk)
239 [ - - - - : 0 : qCWarning(LogInstaller) << "PackageManager could not remove package" << m_packageId << "after a failed installation";
- - - - -
- - - ]
240 : : }
241 : 23 : }
242 : :
243 : :
244 : 48 : {
245 : 48 : QMutexLocker locker(&m_mutex);
246 [ + + ]: 48 : delete m_extractor;
247 [ + - ]: 48 : m_extractor = nullptr;
248 : 48 : }
249 : 48 : }
250 : :
251 : :
252 : 85 : void InstallationTask::checkExtractedFile(const QString &file) noexcept(false)
253 : : {
254 : 85 : ++m_extractedFileCount;
255 : :
256 [ + + ]: 85 : if (m_extractedFileCount == 1) {
257 [ - + ]: 44 : if (file != u"info.yaml")
258 : 0 : throw Exception(Error::Package, "info.yaml must be the first file in the package. Got %1")
259 : 0 : .arg(file);
260 : :
261 [ + - + + : 46 : m_package.reset(PackageInfo::fromManifest(m_extractor->destinationDirectory().absoluteFilePath(file)));
- + ]
262 [ + - + + ]: 42 : if (m_package->id() != m_extractor->installationReport().packageId())
263 : 1 : throw Exception(Error::Package, "the package identifiers in --PACKAGE-HEADER--' and info.yaml do not match");
264 : :
265 : 41 : m_iconFileName = m_package->icon(); // store it separately as we will give away ApplicationInfo later on
266 : :
267 [ - + ]: 41 : if (m_iconFileName.isEmpty())
268 : 0 : throw Exception(Error::Package, "the 'icon' field in info.yaml cannot be empty or absent.");
269 : :
270 : 41 : m_mutex.lock();
271 : 41 : m_packageId = m_package->id();
272 : 41 : m_mutex.unlock();
273 : :
274 : 41 : m_foundInfo = true;
275 [ + - ]: 41 : } else if (m_extractedFileCount == 2) {
276 : : // the second file must be the icon
277 : :
278 : 41 : Q_ASSERT(m_foundInfo);
279 : 41 : Q_ASSERT(!m_foundIcon);
280 : :
281 [ + + ]: 41 : if (file != m_iconFileName)
282 : 2 : throw Exception(Error::Package,
283 : : "The package icon (as stated in info.yaml) must be the second file in the package."
284 : 3 : " Expected '%1', got '%2'").arg(m_iconFileName, file);
285 : :
286 [ + - + - ]: 80 : QFile icon(m_extractor->destinationDirectory().absoluteFilePath(file));
287 : :
288 [ + - - + ]: 40 : if (icon.size() > 256*1024)
289 : 0 : throw Exception(Error::Package, "the size of %1 is too large (max. 256KB)").arg(file);
290 : :
291 : 40 : m_foundIcon = true;
292 : 40 : } else {
293 : 0 : throw Exception(Error::Package, "Could not find info.yaml and the icon file at the beginning of the package.");
294 : : }
295 : :
296 [ + + + - ]: 81 : if (m_foundIcon && m_foundInfo) {
297 : : // we're not interested in any other files from here on...
298 [ + - ]: 40 : m_extractor->setFileExtractedCallback(nullptr);
299 : :
300 : 40 : bool doubleInstallation = false;
301 : 40 : QMetaObject::invokeMethod(PackageManager::instance(), [this, &doubleInstallation]() {
302 : 40 : doubleInstallation = PackageManager::instance()->isPackageInstallationActive(m_packageId);
303 : 40 : }, Qt::BlockingQueuedConnection);
304 [ + + ]: 40 : if (doubleInstallation)
305 : 1 : throw Exception(Error::Package, "Cannot install the same package %1 multiple times in parallel").arg(m_packageId);
306 : :
307 : 39 : QDir oldDestinationDirectory = m_extractor->destinationDirectory();
308 : :
309 [ + + ]: 39 : startInstallation();
310 : :
311 [ + - + - : 114 : QFile::copy(oldDestinationDirectory.filePath(u"info.yaml"_s), m_extractionDir.filePath(u"info.yaml"_s));
+ - ]
312 [ + - + - : 76 : QFile::copy(oldDestinationDirectory.filePath(m_iconFileName), m_extractionDir.filePath(m_iconFileName));
+ - ]
313 : :
314 : 38 : {
315 : 38 : QMutexLocker locker(&m_mutex);
316 [ + - ]: 38 : m_extractor->setDestinationDirectory(m_extractionDir);
317 : :
318 [ + - ]: 38 : QString path = m_extractionDir.absolutePath();
319 [ + - ]: 38 : path.chop(1); // remove the '+'
320 [ + - + - ]: 38 : m_package->setBaseDir(QDir(path));
321 [ + - ]: 38 : }
322 : :
323 : : // we need to call those ApplicationManager methods in the correct thread
324 : : // this will also exclusively lock the application for us
325 : : // m_package ownership is transferred to the ApplicationManager
326 [ + - ]: 38 : QString packageId = m_package->id(); // m_package is gone after the invoke
327 : 38 : QPointer<Package> newPackage;
328 [ + - + - ]: 76 : QMetaObject::invokeMethod(PackageManager::instance(), [this, &newPackage]()
329 : 38 : { newPackage = PackageManager::instance()->startingPackageInstallation(m_package.release()); },
330 : : Qt::BlockingQueuedConnection);
331 : 38 : m_managerApproval = !newPackage.isNull();
332 : :
333 [ - + ]: 38 : if (!m_managerApproval)
334 : 0 : throw Exception("PackageManager declined the installation of %1").arg(packageId);
335 : :
336 [ + - + - : 78 : qCDebug(LogInstaller) << "emit taskRequestingInstallationAcknowledge" << id() << "for package" << packageId;
+ - + - +
- + - + -
+ + ]
337 : :
338 : : // Create temporary objects for QML just for the signal emission.
339 : : // The problem here is that the PackageInfo instance backing the Package object is also
340 : : // temporary and the ownership is with the C++ side of the PackageManager.
341 : : // Ideally we should have kept the 'package' parameter as a dumb QVariantMap, but changing
342 : : // that back would be a huge API break nowadays as the QML APIs are fully typed.
343 : : // At least we have to make sure NOT to change anything in the PackageInfo instance after
344 : : // the signal emission below.
345 [ + - + - : 38 : m_tempPackageForAcknowledge.reset(new Package(newPackage->info(), Package::BeingInstalled));
+ - ]
346 [ + - + - ]: 38 : m_tempPackageForAcknowledge->moveToThread(m_pm->thread());
347 [ + - + - ]: 38 : const auto &applicationInfos = newPackage->info()->applications();
348 [ + + ]: 78 : for (const auto &applicationInfo : applicationInfos) {
349 [ + - + - ]: 40 : auto tempApp = new Application(applicationInfo, m_tempPackageForAcknowledge.get());
350 [ + - + - ]: 40 : tempApp->moveToThread(m_pm->thread());
351 [ + - ]: 40 : m_tempPackageForAcknowledge->addApplication(tempApp);
352 [ + - ]: 40 : m_tempApplicationsForAcknowledge.emplace_back(tempApp);
353 : : }
354 [ + - + - ]: 76 : emit m_pm->taskRequestingInstallationAcknowledge(id(), m_tempPackageForAcknowledge.get(),
355 [ + - + - ]: 76 : m_extractor->installationReport().extraMetaData(),
356 [ + - + - ]: 38 : m_extractor->installationReport().extraSignedMetaData());
357 : :
358 : : // if any of the apps in the package were running before, we now need to wait until all of
359 : : // them have actually stopped
360 [ + - + - : 120 : while (!m_canceled && newPackage && !newPackage->areAllApplicationsStoppedDueToBlock())
+ - + + ]
361 [ + - ]: 2 : QThread::msleep(30);
362 : :
363 [ + - + - ]: 76 : if (m_canceled || newPackage.isNull())
364 : 0 : throw Exception(Error::Canceled, "canceled");
365 : 39 : }
366 : 79 : }
367 : :
368 : 39 : void InstallationTask::startInstallation() noexcept(false)
369 : : {
370 : : // 2. delete old, partial installation
371 : :
372 [ + - + - ]: 39 : QDir installationDir = QString(m_installationPath + u'/');
373 [ + - ]: 39 : QString installationTarget = m_packageId + u'+';
374 [ + - - + ]: 39 : if (installationDir.exists(installationTarget)) {
375 [ # # # # : 0 : if (!removeRecursiveHelper(installationDir.absoluteFilePath(installationTarget)))
# # ]
376 : 0 : throw Exception("could not remove old, partial installation %1/%2").arg(installationDir).arg(installationTarget);
377 : : }
378 : :
379 : : // 4. create new installation
380 [ + - + - : 39 : if (!m_installationDirCreator.create(installationDir.absoluteFilePath(installationTarget)))
+ + ]
381 : 1 : throw Exception("could not create installation directory %1/%2").arg(installationDir).arg(installationTarget);
382 [ + - ]: 38 : m_extractionDir = installationDir; // clazy:exclude=qt6-deprecated-api-fixes
383 [ + - - + ]: 38 : if (!m_extractionDir.cd(installationTarget))
384 : 0 : throw Exception("could not cd into installation directory %1/%2").arg(installationDir).arg(installationTarget);
385 [ + - + - ]: 77 : m_applicationDir.setPath(installationDir.absoluteFilePath(m_packageId));
386 : 39 : }
387 : :
388 : 26 : void InstallationTask::finishInstallation() noexcept(false)
389 : : {
390 : 26 : QDir documentDirectory(m_documentPath);
391 [ + - ]: 26 : ScopedDirectoryCreator documentDirCreator;
392 : :
393 : 26 : enum { Installation, Update } mode = Installation;
394 : :
395 [ + - + + ]: 26 : if (m_applicationDir.exists())
396 : 4 : mode = Update;
397 : :
398 : : // create the installation report
399 [ + - ]: 26 : InstallationReport report = m_extractor->installationReport();
400 : :
401 [ + - + - ]: 52 : QFile reportFile(m_extractionDir.absoluteFilePath(u".installation-report.yaml"_s));
402 [ + - + - : 26 : if (!reportFile.open(QFile::WriteOnly) || !report.serialize(&reportFile))
+ - - + ]
403 : 0 : throw Exception(reportFile, "could not write the installation report");
404 [ + - ]: 26 : reportFile.close();
405 : :
406 : : // create the document directories when installing (not needed on updates)
407 [ + + + - ]: 26 : if ((mode == Installation) && !m_documentPath.isEmpty()) {
408 : : // this package may have been installed earlier and the document directory may not have been removed
409 [ + - + - ]: 22 : if (!documentDirectory.cd(m_packageId)) {
410 [ + - + - : 22 : if (!documentDirCreator.create(documentDirectory.absoluteFilePath(m_packageId)))
+ + ]
411 [ + - ]: 1 : throw Exception(Error::IO, "could not create the document directory %1").arg(documentDirectory.filePath(m_packageId));
412 : : }
413 : : }
414 : :
415 : : // final rename
416 : :
417 : : // POSIX cannot atomically rename directories, if the destination directory exists
418 : : // and is non-empty. We need to do a double-rename in this case, which might fail!
419 : :
420 [ + - ]: 25 : ScopedRenamer renameApplication;
421 : :
422 [ + + ]: 25 : if (mode == Update) {
423 [ + - - + ]: 4 : if (!renameApplication.rename(m_applicationDir, ScopedRenamer::NamePlusToName | ScopedRenamer::NameToNameMinus))
424 : 0 : throw Exception(Error::IO, "could not rename application directory %1+ to %1 (including a backup to %1-)").arg(m_applicationDir);
425 : : } else {
426 [ + - - + ]: 21 : if (!renameApplication.rename(m_applicationDir, ScopedRenamer::NamePlusToName))
427 : 0 : throw Exception(Error::IO, "could not rename application directory %1+ to %1").arg(m_applicationDir);
428 : : }
429 : :
430 : : // from this point onwards, we are not allowed to throw anymore, since the installation is "done"
431 : :
432 [ + - ]: 25 : setState(CleaningUp);
433 : :
434 [ + - ]: 25 : renameApplication.take();
435 [ + - ]: 25 : documentDirCreator.take();
436 : :
437 [ + - ]: 25 : m_installationDirCreator.take();
438 : :
439 : : // this should not be necessary, but it also won't hurt
440 [ + + ]: 25 : if (mode == Update)
441 [ + - + - : 8 : removeRecursiveHelper(m_applicationDir.absolutePath() + u'-');
+ - ]
442 : :
443 : : #ifdef Q_OS_UNIX
444 : : // write files to the filesystem
445 : 25 : sync();
446 : : #endif
447 : :
448 : 25 : m_errorString.clear();
449 : 29 : }
450 : :
451 : : QT_END_NAMESPACE_AM
452 : :
453 : : #include "moc_installationtask.cpp"
|