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

栏目: C · 发布时间: 6年前

内容简介:我们先看个最简单方案,下面的代码会监视/home/work下文件(夹)的新增、删除等操作。第6行,我们使用了默认的loop。除了default_loop,libev还提供了dynamic_loop。如果我们没有指定loop,则libev会使用默认的。第7行,我们声明了文件(夹)监视器state。

我们先看个最简单方案,下面的代码会监视/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


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Introduction to Linear Optimization

Introduction to Linear Optimization

Dimitris Bertsimas、John N. Tsitsiklis / Athena Scientific / 1997-02-01 / USD 89.00

"The true merit of this book, however, lies in its pedagogical qualities which are so impressive..." "Throughout the book, the authors make serious efforts to give geometric and intuitive explanations......一起来看看 《Introduction to Linear Optimization》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

MD5 加密
MD5 加密

MD5 加密工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具