Solarian Programmer

My programming ramblings

C++17 Filesystem - Writing a simple file watcher

Posted on January 13, 2019 by Paul

In this article I will show you how to use the C++17 std::filesystem library to write a simple file watcher or file monitor. The advantage of using the C++17 std::filesystem library is that your code will be portable on all operating systems for which a C++17 compiler is available.

We are going to implement a C++17 file watcher that will monitor a given folder for file changes. For our limited purposes, we’ll monitor only the creation, modification and deletion of all files from the watched directory. The base folder will be checked for changes at regular time intervals and, in case of changes, we’ll run a user defined function.

Disclaimer: The code presented in this article is not meant to be used as is in production. The code was written as an exercise or demo to show what you can do with the C++17 std::filesystem library. If you want the ultimate performance, you should try to use the operating system functions like inotify on Linux or kqueue on macOS and FreeBSD.

At the time of this writing, you can use the C++17 std::filesystem library with GCC 8, Clang 7 and MSVC 2017. Here is an example of compiling a C++ program that uses std::filesystem with GCC:

1 g++ -std=c++17 -Wall -pedantic test_fs_watcher.cpp -o test_fs_watcher -lstdc++fs

and Clang:

1 clang++ -std=c++17 -stdlib=libc++ -Wall -pedantic test_fs_watcher.cpp -o test_fs_watcher -lc++fs

Please note that, at the time this writing, Apple’s Clang from Xcode 10 or the Command Line Tools doesn’t support the std::filesystem library. If you want to install GCC 8 on your macOS check this article.

We’ll start by writing a FileWatcher class that will check a given folder for changes at regular intervals. Here is an example of how I want to be able to use our FileWatcher class:

 1 #include <iostream>
 2 #include "FileWatcher.h"
 3 
 4 int main() {
 5     // Create a FileWatcher instance
 6     FileWatcher fw{ /* ... */ };
 7 
 8     // Start monitoring a folder for changes and (in case of changes)
 9     // run a user provided lambda function
10     fw.start([] (/* ... */) -> void {
11         // ...
12     });
13 }

Let’s start by defining the list of possible file changes (creation, modification, deletion) in FileWatcher.h:

 1 #pragma once
 2 
 3 #include <filesystem>
 4 #include <chrono>
 5 #include <thread>
 6 #include <unordered_map>
 7 #include <string>
 8 #include <functional>
 9 
10 // Define available file changes
11 enum class FileStatus {created, modified, erased};
12 
13 class FileWatcher {
14     // ...
15 };

Next, we can start writing the FileWatcher class. If we want to be able to monitor what file was changed, we’ll need a way to keep a record of the existing files in the watched folder. A simple approach is to use a hash table that will have as keys the file path and as values the time of the last modification of the file. We can use a std::unordered_map to store the above:

 1 #pragma once
 2 
 3 // ...
 4 
 5 class FileWatcher {
 6 public:
 7     std::string path_to_watch;
 8     // Time interval at which we check the base folder for changes
 9     std::chrono::duration<int, std::milli> delay;
10 
11     // Keep a record of files from the base directory and their last modification time
12     FileWatcher(std::string path_to_watch, std::chrono::duration<int, std::milli> delay) : path_to_watch{path_to_watch}, delay{delay} {
13         for(auto &file : std::filesystem::recursive_directory_iterator(path_to_watch)) {
14             paths_[file.path()] = std::filesystem::last_write_time(file);
15         }
16     }
17 
18     // ...
19 
20 private:
21     std::unordered_map<std::string, std::filesystem::file_time_type> paths_;
22 
23     // ...
24 
25 };

Now, we can implement the function that will start monitoring the base folder path_to_watch for changes:

 1 #pragma once
 2 
 3 // ...
 4 
 5 class FileWatcher {
 6 public:
 7     // ...
 8 
 9     // Monitor "path_to_watch" for changes and in case of a change execute the user supplied "action" function
10     void start(const std::function<void (std::string, FileStatus)> &action) {
11         while(running_) {
12             // Wait for "delay" milliseconds
13             std::this_thread::sleep_for(delay);
14 
15             // Check if one of the old files was erased
16             for(auto &el : paths_) {
17                 if(!std::filesystem::exists(el.first)) {
18                     action(el.first, FileStatus::erased);
19                     paths_.erase(el.first);
20                 }
21             }
22 
23             // Check if a file was created or modified
24             for(auto &file : std::filesystem::recursive_directory_iterator(path_to_watch)) {
25                 auto current_file_last_write_time = std::filesystem::last_write_time(file);
26 
27                 // File creation
28                 if(!contains(file.path())) {
29                     paths_[file.path()] = current_file_last_write_time;
30                     action(file.path(), FileStatus::created);
31                 // File modification
32                 } else {
33                     if(paths_[file.path()] != current_file_last_write_time) {
34                         paths_[file.path()] = current_file_last_write_time;
35                         action(file.path(), FileStatus::modified);
36                     }
37                 }
38             }
39         }
40     }
41 private:
42     std::unordered_map<std::string, std::filesystem::file_time_type> paths_;
43     bool running_ = true;
44 
45     // Check if "paths_" contains a given key
46     // If your compiler supports C++20 use paths_.contains(key) instead of this function
47     bool contains(const std::string &key) {
48         auto el = paths_.find(key);
49         return el != paths_.end();
50     }
51 };

The start function will start an infinite loop in which we wait delay milliseconds and than we check for file changes. If a change is detected, we call the user defined action function that receives as parameters the file path, as a string, and the type of change detected.

Please note the contains function that checks if a given key is present in the paths_. The C++20 standard will add a contains member function to std::unordered_map, at which time you can replace line 28 from the above code with something like:

1     if(!paths_.contains(file.path()))

and remove our contains function.

At this point, FileWatcher.h is complete. Here is an example of using it to monitor the current folder for changes every five seconds:

 1 #include <iostream>
 2 #include "FileWatcher.h"
 3 
 4 
 5 int main() {
 6     // Create a FileWatcher instance that will check the current folder for changes every 5 seconds
 7     FileWatcher fw{"./", std::chrono::milliseconds(5000)};
 8 
 9     // Start monitoring a folder for changes and (in case of changes)
10     // run a user provided lambda function
11     fw.start([] (std::string path_to_watch, FileStatus status) -> void {
12         // Process only regular files, all other file types are ignored
13         if(!std::filesystem::is_regular_file(std::filesystem::path(path_to_watch)) && status != FileStatus::erased) {
14             return;
15         }
16 
17         switch(status) {
18             case FileStatus::created:
19                 std::cout << "File created: " << path_to_watch << '\n';
20                 break;
21             case FileStatus::modified:
22                 std::cout << "File modified: " << path_to_watch << '\n';
23                 break;
24             case FileStatus::erased:
25                 std::cout << "File erased: " << path_to_watch << '\n';
26                 break;
27             default:
28                 std::cout << "Error! Unknown file status.\n";
29         }
30     });
31 }

In the above code, the user has supplied a lambda function that will be called when a file change is detected.

You can find the complete source code on the GitHub repository for this article.

Next, I will show you an example of building, running and testing the above code on my machine. I’ve used two Terminal windows, one for interacting with the program and one for file operations. Here is the first window output:

 1 /tmp/cpp17-filewatcher $ g++-8.2 -std=c++17 -Wall -pedantic test_fs_watcher.cpp -o test_fs_watcher -lstdc++fs
 2 /tmp/cpp17-filewatcher $ ./test_fs_watcher
 3 File created: ./test1/t2
 4 File created: ./test1/t1
 5 File created: ./test2/z3
 6 File created: ./test2/z2
 7 File created: ./test2/z1
 8 File modified: ./test1/t2
 9 File erased: ./test1/t1
10 File erased: ./test2/z2
11 ^C
12 /tmp/cpp17-filewatcher $

Second window output:

1 /tmp/cpp17-filewatcher $ mkdir test1
2 /tmp/cpp17-filewatcher $ touch test1/t1 test1/t2
3 /tmp/cpp17-filewatcher $ mkdir test2
4 /tmp/cpp17-filewatcher $ touch test2/z1 test2/z2 test2/z3
5 /tmp/cpp17-filewatcher $ echo "Change a file" > test1/t2
6 /tmp/cpp17-filewatcher $ rm test2/z2 test1/t1
7 /tmp/cpp17-filewatcher $

If you are interested to learn more about modern C++ I would recommend reading A tour of C++ by Bjarne Stroustrup.

or Effective Modern C++ by Scott Meyers.


Show Comments