1// Copyright (C) 2020 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qfileinfogatherer_p.h"
5#include <qcoreapplication.h>
6#include <qdebug.h>
7#include <qdirlisting.h>
8#include <private/qabstractfileiconprovider_p.h>
9#include <private/qfileinfo_p.h>
10#ifndef Q_OS_WIN
11# include <unistd.h>
12# include <sys/types.h>
13#endif
14#if defined(Q_OS_VXWORKS)
15# include "qplatformdefs.h"
16#endif
17
18QT_BEGIN_NAMESPACE
19
20using namespace Qt::StringLiterals;
21
22#ifdef QT_BUILD_INTERNAL
23Q_CONSTINIT static QBasicAtomicInt fetchedRoot = Q_BASIC_ATOMIC_INITIALIZER(false);
24Q_AUTOTEST_EXPORT void qt_test_resetFetchedRoot()
25{
26 fetchedRoot.storeRelaxed(newValue: false);
27}
28
29Q_AUTOTEST_EXPORT bool qt_test_isFetchedRoot()
30{
31 return fetchedRoot.loadRelaxed();
32}
33#endif
34
35static QString translateDriveName(const QFileInfo &drive)
36{
37 QString driveName = drive.absoluteFilePath();
38#ifdef Q_OS_WIN
39 if (driveName.startsWith(u'/')) // UNC host
40 return drive.fileName();
41 if (driveName.endsWith(u'/'))
42 driveName.chop(1);
43#endif // Q_OS_WIN
44 return driveName;
45}
46
47/*!
48 Creates thread
49*/
50QFileInfoGatherer::QFileInfoGatherer(QObject *parent)
51 : QThread(parent)
52 , m_iconProvider(&defaultProvider)
53{
54 start(LowPriority);
55}
56
57/*!
58 Destroys thread
59*/
60QFileInfoGatherer::~QFileInfoGatherer()
61{
62 requestAbort();
63 wait();
64}
65
66bool QFileInfoGatherer::event(QEvent *event)
67{
68 if (event->type() == QEvent::DeferredDelete && isRunning()) {
69 // We have been asked to shut down later but were blocked,
70 // so the owning QFileSystemModel proceeded with its shut-down
71 // and deferred the destruction of the gatherer.
72 // If we are still blocked now, then we have three bad options:
73 // terminate, wait forever (preventing the process from shutting down),
74 // or accept a memory leak.
75 requestAbort();
76 if (!wait(time: 5000)) {
77 // If the application is shutting down, then we terminate.
78 // Otherwise assume that sooner or later the thread will finish,
79 // and we delete it then.
80 if (QCoreApplication::closingDown())
81 terminate();
82 else
83 connect(sender: this, signal: &QThread::finished, context: this, slot: [this]{ delete this; });
84 return true;
85 }
86 }
87
88 return QThread::event(event);
89}
90
91void QFileInfoGatherer::requestAbort()
92{
93 requestInterruption();
94 QMutexLocker locker(&mutex);
95 condition.wakeAll();
96}
97
98void QFileInfoGatherer::setResolveSymlinks(bool enable)
99{
100 Q_UNUSED(enable);
101#ifdef Q_OS_WIN
102 m_resolveSymlinks = enable;
103#endif
104}
105
106void QFileInfoGatherer::driveAdded()
107{
108 fetchExtendedInformation(path: QString(), files: QStringList());
109}
110
111void QFileInfoGatherer::driveRemoved()
112{
113 QStringList drives;
114 const QFileInfoList driveInfoList = QDir::drives();
115 for (const QFileInfo &fi : driveInfoList)
116 drives.append(t: translateDriveName(drive: fi));
117 emit newListOfFiles(directory: QString(), listOfFiles: drives);
118}
119
120bool QFileInfoGatherer::resolveSymlinks() const
121{
122#ifdef Q_OS_WIN
123 return m_resolveSymlinks;
124#else
125 return false;
126#endif
127}
128
129void QFileInfoGatherer::setIconProvider(QAbstractFileIconProvider *provider)
130{
131 m_iconProvider = provider;
132}
133
134QAbstractFileIconProvider *QFileInfoGatherer::iconProvider() const
135{
136 return m_iconProvider;
137}
138
139/*!
140 Fetch extended information for all \a files in \a path
141
142 \sa updateFile(), update(), resolvedName()
143*/
144void QFileInfoGatherer::fetchExtendedInformation(const QString &path, const QStringList &files)
145{
146 QMutexLocker locker(&mutex);
147 // See if we already have this dir/file in our queue
148 qsizetype loc = 0;
149 while ((loc = this->path.lastIndexOf(str: path, from: loc - 1)) != -1) {
150 if (this->files.at(i: loc) == files)
151 return;
152 if (loc == 0)
153 break;
154 }
155
156#if QT_CONFIG(thread)
157 this->path.push(t: path);
158 this->files.push(t: files);
159 condition.wakeAll();
160#else // !QT_CONFIG(thread)
161 getFileInfos(path, files);
162#endif // QT_CONFIG(thread)
163
164#if QT_CONFIG(filesystemwatcher)
165 if (files.isEmpty()
166 && !path.isEmpty()
167 && !path.startsWith(s: "//"_L1) /*don't watch UNC path*/) {
168 if (!watchedDirectories().contains(str: path))
169 watchPaths(paths: QStringList(path));
170 }
171#endif
172}
173
174/*!
175 Fetch extended information for all \a filePath
176
177 \sa fetchExtendedInformation()
178*/
179void QFileInfoGatherer::updateFile(const QString &filePath)
180{
181 QString dir = filePath.mid(position: 0, n: filePath.lastIndexOf(c: u'/'));
182 QString fileName = filePath.mid(position: dir.size() + 1);
183 fetchExtendedInformation(path: dir, files: QStringList(fileName));
184}
185
186QStringList QFileInfoGatherer::watchedFiles() const
187{
188#if QT_CONFIG(filesystemwatcher)
189 if (m_watcher)
190 return m_watcher->files();
191#endif
192 return {};
193}
194
195QStringList QFileInfoGatherer::watchedDirectories() const
196{
197#if QT_CONFIG(filesystemwatcher)
198 if (m_watcher)
199 return m_watcher->directories();
200#endif
201 return {};
202}
203
204void QFileInfoGatherer::createWatcher()
205{
206#if QT_CONFIG(filesystemwatcher)
207 m_watcher = new QFileSystemWatcher(this);
208 connect(sender: m_watcher, signal: &QFileSystemWatcher::directoryChanged, context: this, slot: &QFileInfoGatherer::list);
209 connect(sender: m_watcher, signal: &QFileSystemWatcher::fileChanged, context: this, slot: &QFileInfoGatherer::updateFile);
210# if defined(Q_OS_WIN)
211 const QVariant listener = m_watcher->property("_q_driveListener");
212 if (listener.canConvert<QObject *>()) {
213 if (QObject *driveListener = listener.value<QObject *>()) {
214 connect(driveListener, SIGNAL(driveAdded()), this, SLOT(driveAdded()));
215 connect(driveListener, SIGNAL(driveRemoved()), this, SLOT(driveRemoved()));
216 }
217 }
218# endif // Q_OS_WIN
219#endif
220}
221
222void QFileInfoGatherer::watchPaths(const QStringList &paths)
223{
224#if QT_CONFIG(filesystemwatcher)
225 if (m_watching) {
226 if (m_watcher == nullptr)
227 createWatcher();
228 m_watcher->addPaths(files: paths);
229 }
230#else
231 Q_UNUSED(paths);
232#endif
233}
234
235void QFileInfoGatherer::unwatchPaths(const QStringList &paths)
236{
237#if QT_CONFIG(filesystemwatcher)
238 if (m_watcher && !paths.isEmpty())
239 m_watcher->removePaths(files: paths);
240#else
241 Q_UNUSED(paths);
242#endif
243}
244
245bool QFileInfoGatherer::isWatching() const
246{
247 bool result = false;
248#if QT_CONFIG(filesystemwatcher)
249 QMutexLocker locker(&mutex);
250 result = m_watching;
251#endif
252 return result;
253}
254
255/*! \internal
256
257 If \a v is \c false, the QFileSystemWatcher used internally will be deleted
258 and subsequent calls to watchPaths() will do nothing.
259
260 If \a v is \c true, subsequent calls to watchPaths() will add those paths to
261 the filesystem watcher; watchPaths() will initialize a QFileSystemWatcher if
262 one hasn't already been initialized.
263*/
264void QFileInfoGatherer::setWatching(bool v)
265{
266#if QT_CONFIG(filesystemwatcher)
267 QMutexLocker locker(&mutex);
268 if (v != m_watching) {
269 m_watching = v;
270 if (!m_watching)
271 delete std::exchange(obj&: m_watcher, new_val: nullptr);
272 }
273#else
274 Q_UNUSED(v);
275#endif
276}
277
278/*
279 List all files in \a directoryPath
280
281 \sa listed()
282*/
283void QFileInfoGatherer::clear()
284{
285#if QT_CONFIG(filesystemwatcher)
286 QMutexLocker locker(&mutex);
287 unwatchPaths(paths: watchedFiles());
288 unwatchPaths(paths: watchedDirectories());
289#endif
290}
291
292/*
293 Remove a \a path from the watcher
294
295 \sa listed()
296*/
297void QFileInfoGatherer::removePath(const QString &path)
298{
299#if QT_CONFIG(filesystemwatcher)
300 QMutexLocker locker(&mutex);
301 unwatchPaths(paths: QStringList(path));
302#else
303 Q_UNUSED(path);
304#endif
305}
306
307/*
308 List all files in \a directoryPath
309
310 \sa listed()
311*/
312void QFileInfoGatherer::list(const QString &directoryPath)
313{
314 fetchExtendedInformation(path: directoryPath, files: QStringList());
315}
316
317/*
318 Until aborted wait to fetch a directory or files
319*/
320void QFileInfoGatherer::run()
321{
322 forever {
323 // Disallow termination while we are holding a mutex or can be
324 // woken up cleanly.
325 setTerminationEnabled(false);
326 QMutexLocker locker(&mutex);
327 while (!isInterruptionRequested() && path.isEmpty())
328 condition.wait(lockedMutex: &mutex);
329 if (isInterruptionRequested())
330 return;
331 const QString thisPath = std::as_const(t&: path).front();
332 path.pop_front();
333 const QStringList thisList = std::as_const(t&: files).front();
334 files.pop_front();
335 locker.unlock();
336
337 // Some of the system APIs we call when gathering file infomration
338 // might hang (e.g. waiting for network), so we explicitly allow
339 // termination now.
340 setTerminationEnabled(true);
341 getFileInfos(path: thisPath, files: thisList);
342 }
343}
344
345QExtendedInformation QFileInfoGatherer::getInfo(const QFileInfo &fileInfo) const
346{
347 QExtendedInformation info(fileInfo);
348 if (m_iconProvider) {
349 info.icon = m_iconProvider->icon(fileInfo);
350 info.displayType = m_iconProvider->type(fileInfo);
351 } else {
352 info.displayType = QAbstractFileIconProviderPrivate::getFileType(info: fileInfo);
353 }
354#if QT_CONFIG(filesystemwatcher)
355 // ### Not ready to listen all modifications by default
356 static const bool watchFiles = qEnvironmentVariableIsSet(varName: "QT_FILESYSTEMMODEL_WATCH_FILES");
357 if (watchFiles) {
358 if (!fileInfo.exists() && !fileInfo.isSymLink()) {
359 const_cast<QFileInfoGatherer *>(this)->
360 unwatchPaths(paths: QStringList(fileInfo.absoluteFilePath()));
361 } else {
362 const QString path = fileInfo.absoluteFilePath();
363 if (!path.isEmpty() && fileInfo.exists() && fileInfo.isFile() && fileInfo.isReadable()
364 && !watchedFiles().contains(str: path)) {
365 const_cast<QFileInfoGatherer *>(this)->watchPaths(paths: QStringList(path));
366 }
367 }
368 }
369#endif // filesystemwatcher
370
371#ifdef Q_OS_WIN
372 if (m_resolveSymlinks && info.isSymLink(/* ignoreNtfsSymLinks = */ true)) {
373 QFileInfo resolvedInfo(QFileInfo(fileInfo.symLinkTarget()).canonicalFilePath());
374 if (resolvedInfo.exists()) {
375 emit nameResolved(fileInfo.filePath(), resolvedInfo.fileName());
376 }
377 }
378#endif
379 return info;
380}
381
382/*
383 Get specific file info's, batch the files so update when we have 100
384 items and every 200ms after that
385 */
386void QFileInfoGatherer::getFileInfos(const QString &path, const QStringList &files)
387{
388 // List drives
389 if (path.isEmpty()) {
390#ifdef QT_BUILD_INTERNAL
391 fetchedRoot.storeRelaxed(newValue: true);
392#endif
393 QList<std::pair<QString, QFileInfo>> updatedFiles;
394 auto addToUpdatedFiles = [&updatedFiles](QFileInfo &&fileInfo) {
395 fileInfo.stat();
396 updatedFiles.emplace_back(args: std::pair{translateDriveName(drive: fileInfo), fileInfo});
397 };
398
399 if (files.isEmpty()) {
400 // QDir::drives() calls QFSFileEngine::drives() which creates the QFileInfoList on
401 // the stack and return it, so this list is not shared, so no detaching.
402 QFileInfoList infoList = QDir::drives();
403 updatedFiles.reserve(asize: infoList.size());
404 for (auto rit = infoList.rbegin(), rend = infoList.rend(); rit != rend; ++rit)
405 addToUpdatedFiles(std::move(*rit));
406 } else {
407 updatedFiles.reserve(asize: files.size());
408 for (auto rit = files.crbegin(), rend = files.crend(); rit != rend; ++rit)
409 addToUpdatedFiles(QFileInfo(*rit));
410 }
411 emit updates(directory: path, updates: updatedFiles);
412 return;
413 }
414
415 QElapsedTimer base;
416 base.start();
417 QFileInfo fileInfo;
418 bool firstTime = true;
419 QList<std::pair<QString, QFileInfo>> updatedFiles;
420 QStringList filesToCheck = files;
421
422 QStringList allFiles;
423 if (files.isEmpty()) {
424 // Use QDirListing::IteratorFlags when QFileSystemModel is
425 // changed to use them too
426 constexpr auto dirFilters = QDir::AllEntries | QDir::System | QDir::Hidden;
427 for (const auto &dirEntry : QDirListing(path, {}, dirFilters.toInt())) {
428 if (isInterruptionRequested())
429 break;
430 fileInfo = dirEntry.fileInfo();
431 fileInfo.stat();
432 allFiles.append(t: fileInfo.fileName());
433 fetch(info: fileInfo, base, firstTime, updatedFiles, path);
434 }
435 }
436 if (!allFiles.isEmpty())
437 emit newListOfFiles(directory: path, listOfFiles: allFiles);
438
439 QStringList::const_iterator filesIt = filesToCheck.constBegin();
440 while (!isInterruptionRequested() && filesIt != filesToCheck.constEnd()) {
441 fileInfo.setFile(path + QDir::separator() + *filesIt);
442 ++filesIt;
443 fileInfo.stat();
444 fetch(info: fileInfo, base, firstTime, updatedFiles, path);
445 }
446 if (!updatedFiles.isEmpty())
447 emit updates(directory: path, updates: updatedFiles);
448 emit directoryLoaded(path);
449}
450
451void QFileInfoGatherer::fetch(const QFileInfo &fileInfo, QElapsedTimer &base, bool &firstTime,
452 QList<std::pair<QString, QFileInfo>> &updatedFiles, const QString &path)
453{
454 updatedFiles.emplace_back(args: std::pair(fileInfo.fileName(), fileInfo));
455 QElapsedTimer current;
456 current.start();
457 if ((firstTime && updatedFiles.size() > 100) || base.msecsTo(other: current) > 1000) {
458 emit updates(directory: path, updates: updatedFiles);
459 updatedFiles.clear();
460 base = current;
461 firstTime = false;
462 }
463}
464
465QT_END_NAMESPACE
466
467#include "moc_qfileinfogatherer_p.cpp"
468

source code of qtbase/src/gui/itemmodels/qfileinfogatherer.cpp