使用libev监视文件夹下文件(夹)属性变动的方案和实现

        在《libev源码解析》系列中,我们分析了libev的基本原理。本文我们介绍一套使用libev封装的文件(夹)变动监视方案和实现。(转载请指明出于breaksoftware的csdn博客)

        我们先看个最简单方案,下面的代码会监视/home/work下文件(夹)的新增、删除等操作。

void call_back(ev::stat &w, int revents) {
    std::cout << "watch " << w.path << std::endl;
}

int main() {
    ev::default_loop loop;
    ev::stat state;
    state.set(call_back);
    state.set(loop);
    state.start("/home/work/");
    loop.run();
    return 0;
}

        第6行,我们使用了默认的loop。除了default_loop,libev还提供了dynamic_loop。如果我们没有指定loop,则libev会使用默认的。

        第7行,我们声明了文件(夹)监视器state。

        第8行,将回调函数call_back和监视器关联。

        第9行,将loop和监视器关联。

        第10行,监视器开始监视目录/home/work。

        第11行,让loop运行起来以阻塞住进程。

        这样一旦目录下有文件(夹)变动,则会调用回调函数call_back。

        假如这种方式可以涵盖所有情况,那么也不会存在这篇博文了。因为上述方案存在如下缺陷:

  1. 堵塞主线程
  2. call_back的stat::path一直指向被监视的文件(夹)路径。这样在监控一个文件夹时,如果有子文件(夹)新增或者删除,我们都将无法从回调函数中得知变动的是谁。
  3. 如果监视一个文件夹时发生子文件的复制覆盖行为,将监视不到。

        第1个问题并不严重,我们只要启动一个线程便可解决。第2个问题,我们可以通过对比变动前后的目录结构去解决,也不算太复杂。第3个问题则比较严重了。要解决第三个问题,我们需要对文件夹的监视精细到具体的文件级别,也就是说不是笼统的对某个目录进行监视,而是还要对目录下每个文件进行监视。

        于是对一个文件夹的监视,需要做到:

  1. 监视该文件夹,以获取新增文件(夹)信息。
  2. 监视该文件下所有文件,以获取复制覆盖信息。
  3. 对于新增的文件,需要新增监视。
  4. 对于删除的文件,需要删除监视。
  5. 对于文件夹监视器和文件监视器重复上报的行为(删除文件)需要去重处理。

        由于loop会堵塞住线程,所以我们让一个loop占用一个线程。多个监视器可关联到一个loop。但是监视器和loop的关系存在如下情况:

  1. 如果有多个监视器关联到一个loop,则一个监视器停止后,loop仍会堵塞住线程。
  2. 如果只有一个监视器关联到loop,那这个监视器停止后,loop会从堵塞状态中跳出。

        我希望监视器可以关联到同一个loop,于是对loop做了如下封装

class LibevLoop {
public:
    LibevLoop();
    ~LibevLoop();

    template<class T>
    friend void bind(T& a, LibevLoop* b);

public:
    void run_loop();

private:
    ev::dynamic_loop loop_;
    std::timed_mutex timed_mutex_;
};

template<class T>
void bind(T& a, LibevLoop* b) {
    a.set(b->loop_);
}

LibevLoop::~LibevLoop() {
    loop_.break_loop(ev::ALL);
}

void LibevLoop::run_loop() {
    if (timed_mutex_.try_lock_for(std::chrono::milliseconds(1))) {
        std::thread t([this]{
            timed_mutex_.lock();
            loop_.run(); 
            timed_mutex_.unlock();
        });
        t.detach();
        timed_mutex_.unlock();
    }
}

        由于ev::dynamic_loop是内部管理对象,我并不希望暴露出它,于是提供了一个友元函数bind供外部使用。其实这个地方使用模板函数并不是很合适,最好是针对具体类的方法。

        run_loop函数内部使用超时锁检测loop是否在运行,从而可以保证各个线程调用该函数时只有一个线程被运行。

        我们再封装了一个监视器类

class Watcher {
public:
    using callback = std::function<void(ev::stat&, int)>;
    
    Watcher() = delete;
    explicit Watcher(const std::string& path, callback c, LibevLoop* loop = nullptr);
    ~Watcher();
private:
    void callback_(ev::stat &w, int revents);
private:
    callback cb_;
    ev::stat state_;
};

Watcher::Watcher(const std::string& path, callback c, LibevLoop* loop) {
    if (!loop) {
        static LibevLoop loop_;
        loop = &loop_;
    }
    cb_.swap(c);
    state_.set<Watcher, &Watcher::callback_>(this);
    bind(state_, loop);
    state_.start(path.c_str());
    loop->run_loop();
}

Watcher::~Watcher() {
    state_.stop();
}

void Watcher::callback_(ev::stat &w, int revents) {
    cb_(w, revents);
}

        Watcher的构造函数执行的是文中最开始给出的libev的调用过程。区别是loop被替换为之前定义的LibevLoop,从而不会在该步堵塞线程。

        现在我们可以实现监视器中最基础的文件监视器。

class FileWatcher {
public:
    using callback = std::function<void(const std::string& path, FileWatcherAction action)>;

    FileWatcher() = delete;
    ~FileWatcher();
    explicit FileWatcher(const std::string& path, callback cb, LibevLoop* loop = nullptr);
private:
    void watch_(ev::stat&, int);
private:
    callback cb_;
    std::string file_path_;
    std::time_t last_write_time_ = 0;
    std::shared_ptr<Watcher> watcher_;
};

FileWatcher::~FileWatcher() {
}

FileWatcher::FileWatcher(const std::string& path, callback cb, LibevLoop* loop) {
    file_path_ = absolute(path);
    cb_ = std::move(cb);

    if (boost::filesystem::is_directory(file_path_)) {
        return;
    }

    if (boost::filesystem::is_regular_file(file_path_)) {
        last_write_time_ = boost::filesystem::last_write_time(file_path_);
    }

    watcher_ = std::make_shared<Watcher>(file_path_, 
        std::bind(&FileWatcher::watch_, this, std::placeholders::_1, std::placeholders::_2), loop);
}

void FileWatcher::watch_(ev::stat &w, int revents) {
    if (!boost::filesystem::is_regular_file(file_path_)) {
        if (last_write_time_ != 0) {
            cb_(file_path_, FILE_DEL);
        }
        return;
    }

    std::time_t t = boost::filesystem::last_write_time(file_path_);
    if (last_write_time_ != t) {
        FileWatcherAction ac = (last_write_time_ == 0) ? FILE_NEW : FILE_MODIFY;
        cb_(file_path_, ac);
    }
}

        由于libev需要监视的路径是绝对路径,所以FileWatcher函数会先通过absolute函数修正路径。

std::string absolute(const std::string& path) {
    if (boost::filesystem::path(path).is_absolute()) {
        return path;
    }
    std::string absolute_path = boost::filesystem::system_complete(path).string();
    return absolute_path;
}

        然后获取该文件的最后修改时间。

        FileWatcher::watch_函数是回调函数,它一开始检测文件是否存在,如果不存在且之前存在(最后修改时间不为0),则发起通知。如果文件存在,则通过通过对比最后修改时间来确定发生的行为是“新增”还是“修改”。

        接下来就要接触到比较复杂的文件夹监视。之前我们提到过,需要对目录下所有文件进行监视,并且需要遍历整个目录以确定新增的是哪个文件。于是就设计了一个遍历目录的方法

using callback = std::function<void(const std::string&)>;

void folder_scan(const std::string& path, callback file_cb, callback folder_cb) {
    if (!boost::filesystem::is_directory(path)) {
        return;
    }
    
    if (!boost::filesystem::exists(path)) {
        return;
    }

    boost::filesystem::directory_iterator it(path);
    boost::filesystem::directory_iterator end;
    for (; it != end; it++) {
        if (boost::filesystem::is_directory(*it)) {
            folder_cb(it->path().string());
            folder_scan(it->path().string(), file_cb, folder_cb);
        }
        else {
            file_cb(it->path().string());
        }
    }
}

        folder_scan方法提供了两个回调,一个是在扫描到文件时调用,一个是扫描到文件夹时调用。

        对比文件夹下文件(夹)新增的类将使用上述方法实现对比操作。

enum PathType {
    E_FILE = 0,
    E_FOLDER,
};

struct PathInfo {
    std::string path;
    PathType type;
    bool operator < (const PathInfo & right) const {
        return path.compare(right.path) < 0;
    }
};

using PathInfoSet = std::set<PathInfo>;

class FolderDiff {
public:
    explicit FolderDiff(const std::string& path);
    void diff(PathInfoSet & add, PathInfoSet & remove);
private:
    void scan_(const std::string& path, PathType type, PathInfoSet& path_infos);
private:
    std::string folder_path_;
    PathInfoSet path_infos_;
};

FolderDiff::FolderDiff(const std::string& path){
    folder_path_ = absolute(path);

    PathInfoSet path_infos;
    folder_scan(folder_path_, 
        std::bind(&FolderDiff::scan_, this, std::placeholders::_1, E_FILE, std::ref(path_infos_)),
        std::bind(&FolderDiff::scan_, this, std::placeholders::_1, E_FOLDER, std::ref(path_infos_)));
}

void FolderDiff::scan_(const std::string& path, PathType type, PathInfoSet& path_infos) {
    PathInfo pi{path, type};
    path_infos.insert(pi);
}

void FolderDiff::diff(PathInfoSet & add, PathInfoSet & remove) {
    PathInfoSet path_infos;
    folder_scan(folder_path_, 
        std::bind(&FolderDiff::scan_, this, std::placeholders::_1, E_FILE, std::ref(path_infos)),
        std::bind(&FolderDiff::scan_, this, std::placeholders::_1, E_FOLDER, std::ref(path_infos)));

    std::set_difference(path_infos.begin(), path_infos.end(),
                        path_infos_.begin(), path_infos_.end(), std::inserter(add, add.begin()));

    std::set_difference(path_infos_.begin(), path_infos_.end(),
                    path_infos.begin(), path_infos.end(), std::inserter(remove, remove.begin()));
    path_infos_ = path_infos;
}

        Folder::diff方法将计算出和之前目录状态的对比结果。

        FolderWatcher是最终实现文件夹监视的类。它的构造函数第8行构建了一个文件夹对比类;第10行遍历整个目录,对目录下文件夹和文件设置监视器。由于子文件夹不用监视,所以文件夹监视函数watch_folder_实际什么都没干。第14行启动了path路径文件夹监视器。

FolderWatcher::FolderWatcher(const std::string& path, callback c, LibevLoop* loop) {
    folder_path_ = absolute(path);
    if (boost::filesystem::is_regular_file(folder_path_)) {
        return;
    }

    cb_ = std::move(c);
    fdiff_ = std::make_shared<FolderDiff>(folder_path_);

    folder_scan(folder_path_, 
        std::bind(&FolderWatcher::watch_file_, this, std::placeholders::_1),
        std::bind(&FolderWatcher::watch_folder_, this, std::placeholders::_1));

    watcher_ = std::make_shared<Watcher>(folder_path_, 
        std::bind(&FolderWatcher::watch_, this, 
            std::placeholders::_1, std::placeholders::_2), loop);
}

void FolderWatcher::watch_folder_(const std::string& path) {
}

        对每个子文件的监视使用watch_file_回调,它在底层使用了之前定义的FileWatcher文件监视器类。

void FolderWatcher::watch_file_(const std::string& path){
    std::unique_lock<std::mutex> lock(mutex_);
    files_last_modify_time_[path] = boost::filesystem::last_write_time(path);
    file_watchers_[path] = std::make_shared<FileWatcher>(path, 
        std::bind(&FolderWatcher::file_watcher_, this, 
            std::placeholders::_1, std::placeholders::_2));
}

void FolderWatcher::file_watcher_(const std::string& path, FileWatcherAction action) {
    PathInfo pi{path, E_FILE};
    WatcherAction ac = (WatcherAction)action; 
    notify_filewatcher_change_(pi, ac);
}

        对主目录的监视使用watch_回调函数,它内部是通过之前定义的FolderDiff类实现的。

void FolderWatcher::watch_(ev::stat &w, int revents) {
    PathInfoSet add;
    PathInfoSet remove;
    fdiff_->diff(add, remove);
    for (auto& it : add) {
        notify_folderwatcher_change_(it, true);
    }

    for (auto& it : remove) {
        notify_folderwatcher_change_(it, false);
    }
}

void FolderWatcher::notify_folderwatcher_change_(const PathInfo& pi, bool add) {
    if (pi.type == E_FOLDER) {
        cb_(pi, add ? NEW : DEL);
    }
    else {
        notify_filewatcher_change_(pi, add ? NEW : DEL);
    }
}

        如果新增的文件夹,则直接调用回调函数;否则使用notify_filewatcher_change方法去通知。

        notify_filewatcher_change方法比较复杂,它底层调用的change_filewatchers_方法根据文件的新增和删除来管理文件监视器。

void FolderWatcher::change_filewatchers_(const std::string& path, WatcherAction action) {
    if (action == DEL) {
        std::unique_lock<std::mutex> lock(mutex_);
        auto it = file_watchers_.find(path);
        if (it != file_watchers_.end()) {
            file_watchers_.erase(it);
        }
    }
    else if (action == NEW) {
        std::unique_lock<std::mutex> lock(mutex_);
        auto it = file_watchers_.find(path);
        if (it != file_watchers_.end()) {
            file_watchers_.erase(it);
        }
        file_watchers_[path] = std::make_shared<FileWatcher>(path, 
            std::bind(&FolderWatcher::file_watcher_, this, 
                std::placeholders::_1, std::placeholders::_2));
    }
}

        由于对于文件的删除行为,文件监视器和文件夹监视器都会上报,所以需要对其进行去重。于是我们使用最后修改时间来做统一。

void FolderWatcher::notify_filewatcher_change_(const PathInfo& pi, WatcherAction action) {
    change_filewatchers_(pi.path, action);

    bool notify = true;
    if (action == DEL) {
        std::unique_lock<std::mutex> lock(mutex_);
        auto it = files_last_modify_time_.find(pi.path);
        if (it == files_last_modify_time_.end()) {
            notify = false;
        }
        else {
            files_last_modify_time_.erase(it);
        }
    }
    else if (action == NEW || action == MODIFY) {
        std::unique_lock<std::mutex> lock(mutex_);
        auto it = files_last_modify_time_.find(pi.path);
        if (it != files_last_modify_time_.end()) {
            if (boost::filesystem::last_write_time(pi.path) == it->second) {
                notify = false;
            }
        }
        else {
            files_last_modify_time_[pi.path] = boost::filesystem::last_write_time(pi.path);
        }
    }
    
    if (notify) {
        cb_(pi, action);
    }
}

        最后需要指出的是,这套代码,在不同的操作系统上有不一致的行为。比如在Centos上,如果我们监视一个不存在的文件路径,然后新建该文件,则会发起通知。而在Ubuntu上,该行为则监视不到。但是这个差异也可以理解。

        最后附上代码库,其中的单元测试基于Centos的。https://github.com/f304646673/filewatcher.git

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Zaqdt_ACM

牛客练习赛38 B. 出题人的女装(条件概率)

题目链接:https://ac.nowcoder.com/acm/contest/358/B

10620
来自专栏钱塘小甲子的博客

QuantLib教程(一)QuantLib的时间

        QuantLib是一个用于衍生品定价、分析分析的一个库,是用C++写的,通过SWING技术可以用Python调用。量化投资自古分P宗和Q宗,相比...

33220
来自专栏面朝大海春暖花开

button元素的id与onclick的函数名字相同 导致方法失效的问题

需求需要在原先页面添加一个按钮,触发一个function,如此简单的操作,却无意间发现了一个问题。(还是对html了解的太少)

18830
来自专栏yukong的小专栏

56、合并区间 (Merge Intervals)

17130
来自专栏娱乐心理测试

关于前端处理表情符号问题(解决方案)

今天测试反馈一个问题,说是有表情符号的评论上传报错,很显然后台对于表情符号没有做相关的处理,让他们处理,他们说怎样怎样麻烦,算了,还是前端自己处理吧!

20020
来自专栏数据结构与算法

BZOJ2337: [HNOI2011]XOR和路径(期望 高斯消元)

设\(f[i]\)表示从\(i\)到\(n\)边权为1的概率,统计答案的时候乘一下权值

12240
来自专栏中科院渣渣博肆僧一枚

python中的列表

列表是由一系列特定顺序排列的元素组成。你可以创建包含字母表中所有字母,数字0~9或所有家庭成员姓名的列表;也可以将任何东西加入列表中,其中的元素之间可以没有任何...

19530
来自专栏钱塘小甲子的博客

用Excel进行基金业绩评价

        基金业绩评价这种事,无非也就是那么几个指标,Sharpe ratio,Treynor Ratio,InformationRatio,Jensen...

31570
来自专栏Java架构师进阶

Kafka Producer Consumer

org.apache.kafka.clients.producer.KafkaProducer

11830
来自专栏程序生活

机器学习(十六)特征工程之数据分箱

数据分箱(也称为离散分箱或分段)是一种数据预处理技术,用于减少次要观察误差的影响,是一种将多个连续值分组为较少数量的“分箱”的方法。

2.1K20

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励