libevを使用してフォルダの下のファイル(クリップ)属性の変動を監視するシナリオと実装

14850 ワード

『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が呼び出されます.
もしこのような方法がすべての状況をカバーすることができれば、このブログは存在しません.上記のスキームには以下のような欠陥があるからである.
  • メインスレッド
  • を塞ぐ
  • call_backのstat::pathは常に監視されているファイル(クリップ)の経路を指しています.これにより、フォルダを監視するときに、サブファイル(クリップ)が追加または削除された場合、コールバック関数から変更された人が誰なのかを知ることができません.
  • サブフォルダの下にファイルが追加されて監視できません.
  • フォルダを監視している間に発生したファイルのコピーオーバーライドが監視されていない場合は、監視できません.

  • 1つ目の問題は深刻ではありません.スレッドを1つ起動すれば解決できます.2つ目の問題は,変動前後のディレクトリ構造を比較することで解決できるが,あまり複雑ではない.3つ目の問題は、各サブディレクトリを監視し、新しいフォルダが作成されたときに監視を追加し、フォルダが削除されたときに監視を削除する必要があります.4つ目の問題は深刻です.4つ目の問題を解決するには、フォルダの監視を特定のファイルレベルに細かくする必要があります.つまり、ディレクトリの下にあるファイルごとに監視するのではなく、ディレクトリの下にあるファイルごとに監視する必要があります.
    フォルダを監視するには、次のようにします.
  • フォルダを監視して、新しいファイル(クリップ)情報を取得します.
  • は、フォルダの下にあるすべてのサブファイルを監視し、レプリケーションオーバーライド情報を取得する.
  • このフォルダの下にあるすべてのサブフォルダを監視して、サブフォルダの下にあるファイルの新規作成とその後の操作を監視します.
  • 新規ファイル(クリップ)については、新規監視が必要です.
  • 削除されたファイル(クリップ)については、監視を削除する必要があります.
  • フォルダモニタとファイルモニタが繰り返し報告する動作(ファイルの削除)については、再処理が必要です.
  • loopはスレッドを塞ぐので、1つのloopに1つのスレッドを占有させます.複数のモニタを1つのloopに関連付けることができます.しかし、モニタとloopの関係は次のような場合があります.
  • 複数のモニタが1つのloopに関連付けられている場合、1つのモニタが停止しても、loopはスレッドを塞ぐ.
  • モニタが1つだけloopに関連付けられている場合、このモニタが停止すると、loopは閉塞状態から飛び出します.

  • モニタを同じloopに関連付けることを望んで、loopに対して以下のパッケージをしました
    class LibevLoop {
    public:
        LibevLoop();
        ~LibevLoop();
    
        template
        friend void bind(T& a, LibevLoop* b);
    
    public:
        void run_loop();
    
    private:
        ev::dynamic_loop loop_;
        std::timed_mutex timed_mutex_;
    };
    
    template
    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が実行されているかどうかを検出することで、各スレッドが関数を呼び出すときに1つのスレッドだけが実行されることを保証できます.
    モニタクラスをもう1つカプセル化しました
    class Watcher {
    public:
        using callback = std::function;
        
        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(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;
    
        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_;
    };
    
    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(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 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メソッドは、ファイルをスキャンするときに呼び出され、フォルダをスキャンするときに呼び出される2つのコールバックを提供します.
    比較フォルダの下にファイル(クリップ)が追加されたクラスは、上記の方法で比較操作を行います.
    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;
    
    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行目はディレクトリ全体を巡り、ディレクトリの下のフォルダとファイルにモニタを設定します.サブフォルダも監視するのでfolder_watchers_すべてのサブフォルダのモニタが保存されています.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(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(folder_path_, 
            std::bind(&FolderWatcher::watch_, this, 
                std::placeholders::_1, std::placeholders::_2), loop);
    }
    
    void FolderWatcher::watch_folder_(const std::string& path) {
        std::unique_lock<:mutex> lock(mutex_);
        folder_watchers_[path] = std::make_shared(path,
            std::bind(&FolderWatcher::watch_, this,
                std::placeholders::_1, std::placeholders::_2));
    }

    各サブファイルの監視にwatch_を使用するfile_コールバックは、以前定義したFileWatcherファイルモニタクラスを下位層で使用します.
    void FolderWatcher::watch_file_(const std::string& path){
        std::unique_lock<:mutex> lock(mutex_);
        files_last_modify_time_[path] = boost::filesystem::last_write_time(path);
        file_watchers_[path] = std::make_shared(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_change_(it, true);
        }
    
        for (auto& it : remove) {
            notify_change_(it, false);
        }
    }
    
    void FolderWatcher::notify_change_(const PathInfo& pi, bool add) {
        if (pi.type == E_FOLDER) {
            notify_folderwatcher_change_(pi, add ? NEW : DEL);
        }
        else {
            notify_filewatcher_change_(pi, add ? NEW : DEL);
        }
    }

    変更されたことフォルダの場合はnotify_を使用します.folderwatcher_change_メソッド処理;ファイルの場合はnotify_を使用します.filewatcher_changeメソッド処理.
            notify_folderwatcher_change_新しい動作または削除された動作に基づいてfolderを維持するだけの方法は簡単です.watchers_モニタはテーブルをマッピングし、コールバック通知を呼び出します.
    void FolderWatcher::notify_folderwatcher_change_(const PathInfo& pi, WatcherAction action) {
        bool notify = true;
        if (action == DEL) {
            std::unique_lock<:mutex> lock(mutex_);
            auto it = folder_watchers_.find(pi.path);
            if (it == folder_watchers_.end()) {
                notify = false;
            }
            else {
                folder_watchers_.erase(it);
            }
        }
        else if (action == NEW || action == MODIFY) {
            std::unique_lock<:mutex> lock(mutex_);
            auto it = folder_watchers_.find(pi.path);
            if (it == folder_watchers_.end()) {
                folder_watchers_[pi.path] = std::make_shared(pi.path,
                    std::bind(&FolderWatcher::watch_, this,
                        std::placeholders::_1, std::placeholders::_2));
            }
        }
    
        if (notify) {
            cb_(pi, action);
        }
    }

            notify_filewatcher_changeメソッドは複雑で、下位レベルで呼び出されたchange_filewatchers_メソッドは、ファイルの新規作成と削除に基づいてファイルモニタを管理します.
    void FolderWatcher::change_filewatchers_(const std::string& path, WatcherAction action) {
        if (action == DEL) {
            std::unique_lock<: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<:mutex> lock(mutex_);
            auto it = file_watchers_.find(path);
            if (it != file_watchers_.end()) {
                file_watchers_.erase(it);
            }
            file_watchers_[path] = std::make_shared(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<: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<: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