3

qBittorrent目录监视功能在Linux下是否支持NTFS文件系统?

 2 years ago
source link: https://ttys3.dev/post/does-qbittorrent-directory-watch-support-ntfs/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

June 8, 2020

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.cppsrc/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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK