- Published on
qBittorrent目录监视功能在Linux下是否支持NTFS文件系统?
- Authors
- Name
- ttyS3
English title: Does qBittorrent Directory Watch Support NTFS?
qBittorrent 是一个基于rb_libtorrent库 的跨平台高性能BT客户端。
这个libtorrent
有一个前缀rb_
的原因是,有一个叫做RTorrent的软件已经占用了libtorrent
这个名字。
而在qB和Deluge里面,通常大家所说的libtorrent
,全名是libtorrent-rasterbar
, 也就是 RHEL包名里的rb_libtorrent
缘由
今天有群友说qB的目录监视功能对于NTFS文件系统下的目录不工作,然后我回复说, Linux下的inotify是无法支持NTFS文件系统的,并建议其更换为EXT4文件系统。 然后有群友反馈说,根据他的使用经验,qB Linux版对于NTFS文件的watch功能一直是工作正常的。当然,一开始我对此是表示怀疑的,但是此群友随即截图表示,测试了下,再次确认是工作的。
(先说下结果: 后面我也确认了,qBittorrent目录监视功能在Linux下是支持NTFS文件系统的, 那位监视功能不工作的群友,应该是配置不正确导致的)
当然,在此之前我并没有看过qB关于这一块的代码。于是我决定花些时间一探究竟。
qB的种子目录监视功能实现分析
qB是基于 qt5 开发的,而 qt5 的文件监视功能主要是由 QFileSystemWatcher 实现。
于是我又查看了一下QFileSystemWatcher
的源码相关实现 https://github.com/qt/qtbase/blob/69795835f3a578f60b16f09943feee6326087342/src/corelib/io/qfilesystemwatcher.cpp#L50 , 确认了在Linux下,QFileSystemWatcher
实际上是基于inotify
来实现的。
老灯没有看到任何文档表明inotify
支持NTFS文件系统(荒野注:后续的测试表明,这个猜想是错误的。), 因为 libinotify 主要是基于inode
工作的, NTFS 并不存在 inode
这一概念。 同时这里有个类似的回复刚好证实老灯的猜测: https://bugs.launchpad.net/drapes/+bug/110117
既然 qt5 在 Linux 下是肯定不能 watch NTFS 文件系统的,那么我想 qB 肯定是采用了其它实现。(荒野注:后续的测试表明,这个猜想是错误的。)
很快定位到 qB 的相关源码 src/base/filesystemwatcher.cpp
和 src/base/filesystemwatcher.h
我们看一下添加监视目录的相关实现:
namespace
{
// 监视定时器 间隔时间, 10秒(同时用于 网络文件系统 和 不完整种子 ,注意本地文件系统用的是singleShot, 并且间隔固定为2秒)
const int WATCH_INTERVAL = 10000; // 10 sec
// 不完整种子 检测次数限制, 超过5次
const int MAX_PARTIAL_RETRIES = 5;
}
// 首先`FileSystemWatcher` 是继承自 `QFileSystemWatcher`的
FileSystemWatcher::FileSystemWatcher(QObject *parent)
: QFileSystemWatcher(parent)
{
// 来自 QFileSystemWatcher 的事件通知,当目录有变动时, 执行 scanLocalFolder
connect(this, &QFileSystemWatcher::directoryChanged, this, &FileSystemWatcher::scanLocalFolder);
// m_partialTorrentTimer 定时器用于处理“不完整种子”(主要的场景是,一个种子比较大,刚好在copy的时候被发现了,但是此时种子还没有write完)
// 将 m_partialTorrentTimer 设置成single-shot timer, 这种定时器只会fire一次
m_partialTorrentTimer.setSingleShot(true);
connect(&m_partialTorrentTimer, &QTimer::timeout, this, &FileSystemWatcher::processPartialTorrents);
// m_watchTimer 用于处理网络文件系统(比如nfs之类的)下的目录监视, 这个QTimer是会不断定时触发的(每10秒)
connect(&m_watchTimer, &QTimer::timeout, this, &FileSystemWatcher::scanNetworkFolders);
}
void FileSystemWatcher::addPath(const QString &path)
{
if (path.isEmpty()) return;
// Q_OS_HAIKU 这个咱也不用管,没怎么听过的操作系统。
// 由于我们这里讨论的是 Linux 系统,因此可以忽视这个 macro .
#if !defined Q_OS_HAIKU
const QDir dir(path);
// 目录不存在则直接返回
if (!dir.exists()) return;
// 针对网络文件系统处理
// Check if the path points to a network file system or not
if (Utils::Fs::isNetworkFileSystem(path)) {
// Network mode
LogMsg(tr("Watching remote folder: \"%1\"").arg(Utils::Fs::toNativePath(path)));
m_watchedFolders << dir;
// 新目录加入watch列表后,马上 启动 或 重启 timer
m_watchTimer.start(WATCH_INTERVAL);
// 返回
return;
}
#endif
// 正常模式,针对本地文件系统
// Normal mode
LogMsg(tr("Watching local folder: \"%1\"").arg(Utils::Fs::toNativePath(path)));
// 注意这里的addPath是QFileSystemWatcher的,因此,对于NTFS肯定是不生效
QFileSystemWatcher::addPath(path);
// 调用 scanLocalFolder 进行处理
scanLocalFolder(path);
}
我们再看一下scanLocalFolder
的实现:
void FileSystemWatcher::scanLocalFolder(const QString &path)
{
// 直接启动一个single-shot timer, 这种定时器只会fire一次,时间为 2秒 种之后
QTimer::singleShot(2000, this, [this, path]() { processTorrentsInDir(path); });
}
然后我们再看 processTorrentsInDir
的实现:
void FileSystemWatcher::processTorrentsInDir(const QDir &dir)
{
QStringList torrents;
const QStringList files = dir.entryList({"*.torrent", "*.magnet"}, QDir::Files);
for (const QString &file : files) {
const QString fileAbsPath = dir.absoluteFilePath(file);
// .magnet 后缀的文件,主要是用于 Vuze 客户端的一种文件格式
// 对于 qB 或 transmission 用户来说,基本上不太可能遇到这种文件
if (file.endsWith(".magnet", Qt::CaseInsensitive))
torrents << fileAbsPath;
else if (BitTorrent::TorrentInfo::loadFromFile(fileAbsPath).isValid()) // 对于 .torrent 后缀的文件,加载并判断合法性
torrents << fileAbsPath;
else if (!m_partialTorrents.contains(fileAbsPath)) // 不是合法的torrent文件,程序认为它是一个局部种子文件,即不完整的,主要原因是考虑到io速度,这个文件可能还没有写完就在读取了
m_partialTorrents[fileAbsPath] = 0; // 局部文件,添加到 m_partialTorrents 这个 hash表,key 为路径,value为检测次数
}
// 找到种子了(不管是新是旧), fire 一个 torrentsAdded 信号, 然后qB另外部分的代码收到这个信号,就会开始新种子下载了
if (!torrents.empty())
emit torrentsAdded(torrents);
// 如果局部种子hash表非空 并且 m_partialTorrentTimer 定时器 是非活跃 not running (pending), 则启动一个定时器
// 之所以要启动,前面我们已经分析过了,m_partialTorrentTimer 在构造方法里,被设置成了 single-shot timer
if (!m_partialTorrents.empty() && !m_partialTorrentTimer.isActive())
m_partialTorrentTimer.start(WATCH_INTERVAL);
}
所以,在不存在“局部种子”的情况下, 本地文件系统下的被 watch 的目录,只会在qB添加这个目录的时候被扫描一次,后续的任务都交给了QFileSystemWatcher::directoryChanged
信号。 收到这个信号就会重新扫描一次被 watch 的目录。
所以, qB 并没有做其它特殊的处理,它完全依赖 QFileSystemWatcher 本身的机制 (而 QFileSystemWatcher 又是依赖 inotify )。
然而,事情的真相真的是这样么?
我基于一个gist 修改了下,做了一个简单的测试demo。
代码仓库在这 https://github.com/ttys3/qt_directory_watcher
watcher.pro
文件内容如下:
#-------------------------------------------------
#
# Project created by QtCreator 2020-06-08T16:24:38
#
#-------------------------------------------------
QT += core widgets
QT -= gui
TARGET = watcher
CONFIG += console
CONFIG -= app_bundle
TEMPLATE = app
SOURCES += qt_directory_watcher.cpp
qt_directory_watcher.cpp
内容如下:
// ----------------------------------------------
// List the contents of a directory if changed
// Using QFileSystemWatcher, QDirIterator and
// QEventLoop, and lambda function for connect
// ----------------------------------------------
#include <QApplication>
#include <QObject>
#include <QEventLoop>
#include <QDebug>
#include <QFileSystemWatcher>
#include <QDirIterator>
void listDirectoryContents( const QString& dir ) noexcept
{
QFileSystemWatcher watcher;
watcher.addPath( dir );
QEventLoop loop;
QObject::connect( &watcher, &QFileSystemWatcher::directoryChanged,
[]( const QString& path )
{
qDebug() << "\n----------------------------------------------------------";
QDirIterator it( path,
{ "*.torrent" }, // Filter: *.torrent
QDir::Files ); // Files only
while ( it.hasNext() ) // List all txt files
{ // on console
qDebug() << it.next();
}
});
// QObject::connect( &watcher, &QFileSystemWatcher::directoryChanged, &loop, &QEventLoop::quit );
loop.exec();
}
// Example
int main(int argc, char** argv)
{
if (argc != 2) {
qDebug() << "usage: " << *argv << "path";
return -1;
}
QCoreApplication app(argc, argv);
const QString dir { *(argv+1) };
listDirectoryContents( dir );
app.exec();
return 0;
}
构建:
qmake-qt5
make
测试运行:
./watcher /run/media/ttys3/sdc-media/game/for-watch
这里的/run/media/ttys3/sdc-media/game/for-watch
是 NTFS 移动磁盘中的一个目录。 测试的结果表明, QFileSystemWatcher
完全能够监视 NTFS 文件系统中的文件。
于是我重新看了下挂载参数:
❯ mount | grep /run/media/ttys3/sdc-media
/dev/sdd5 on /run/media/ttys3/sdc-media type fuseblk (rw,nosuid,nodev,relatime,user_id=0,group_id=0,default_permissions,allow_other,blksize=4096,uhelper=udisks2)
没错, 这个NTFS分区,是以 fuseblk 文件系统的方式挂载上的。具体的实现是由 ntfs-3g实现的。
下载源码看了下,唯一跟这个相关的调用是src/lowntfs-3g.c
中的fuse_lowlevel_notify_inval_inode
调用。
所以,应该是 ntfs-3g 实现的 fuseblk 已经支持 notify 了, 而内核的 inotify 也有相应的支持。
因此, QFileSystemWatcher
完全不用针对 NTFS 做出任何特别的判断,它只需要照单接收即可。
结论
Linux 下以 fuseblk 或 fuse 方式挂载的 NTFS 文件系统 是支持 inotify 的, 因此 qB 能监视 这种方式挂载的 NTFS 目录。 而对于网络文件系统(比如 nfs 和 smb 之类的),qB 需要用定时器每隔一段时间对需要监视的目录进行扫描。
参考
https://www.tuxera.com/community/open-source-ntfs-3g/
https://github.com/libfuse/libfuse/wiki/Fsnotify-and-FUSE
https://blog.rburchell.com/2012/01/qfilesystemwatcher-internals-in-qt-5.html
https://www.kernel.org/doc/html/latest/filesystems/fuse.html
https://www.kernel.org/doc/Documentation/filesystems/fuse.txt
https://www.kernel.org/doc/html/latest/filesystems/inotify.html
https://www.kernel.org/doc/html/latest/filesystems/ntfs.html
https://libfuse.github.io/doxygen/notify__inval__inode_8c.html