Table of Contents:
- Introduction (1/11)
- Goals and Architecture (2/11)
- The inotify API (3/11)
- File Watcher, C++ (4/11)
- File Watcher, Bash (5/11)
- Execution Engine (6/11)
- Containerization (7/11)
- Kubernetes (8/11)
- Demo (9/11)
- Conclusion (10/11)
- System Setup (11/11)
- Source code
The compiler system, as currently described and designed in the previous posts, relies on being able to respond to user input, i.e. when a user adds in their source file to the queue (folder). Accomplishing this requires monitoring the input folder for changes and beginning the compilation and execution process when a new source file has been detected. Given that this project will be mostly in C++, there are multiple ways of implementing this feature, ranging from platform independent solutions such as std::filesystem to platform-specific ones like inotify for Linux or FindFirstChangeNotification for Windows.
As mentioned in the previous post, we are targeting the Linux platform so the inotify API seems like a natural and simple solution. This API is responsible for monitoring changes on the filesystem and notifying registered applications of those changes, which is exactly what we want. The inotify API exposes three functions: inotify_init, inotify_add_watch, and inotify_rm_watch. The first function creates an inotify instance and returns a file descriptor to it. The next two functions take this file descriptor and allow you to add or remove a watch to a directory – a watch here meaning a monitor to filesystem changes.
A mask is passed in to inotify_add_watch which specifies what type of events to notify on, i.e. file or directory move, close, open, delete, etc. The full list of flags can be found on the link for the inotify page above. Since we are interested in notifying when a user has added their file to the input folder, the mask that is needed is IN_CLOSE_WRITE. After the watch is added on the directory, the application will receive an inotify_event each time a file is added to the folder. This inotify_event structure has the following definition:
struct inotify_event {
int wd; /* Watch descriptor */
uint32_t mask; /* Mask describing event */
uint32_t cookie; /* Unique cookie associating
related events (for rename(2)) */
uint32_t len; /* Size of name field */
char name[]; /* Optional null-terminated name */
};
This event tells us all we need to know: which watch triggered the event, which mask this event corresponds to, as well as the file name. We will listen to these events in a callback and process them as they come in. Sample code is shown below for how to use this API. This code will serve as the template how the file watcher component, covered in the next series of posts, will be implemented.
#include <array>
#include <climits>
#include <iostream>
#include <sys/inotify.h>
#include <unistd.h>
void MonitorDirectoryChange(const int notifyFileDescriptor)
{
constexpr auto BUFFER_SIZE = (10 * (sizeof(struct inotify_event) + NAME_MAX + 1));
std::array<char, BUFFER_SIZE> readBuffer;
while (true)
{
auto bytesRead = read(notifyFileDescriptor, readBuffer.data(), readBuffer.size());
if (bytesRead == -1)
{
perror("read");
break;
}
for (auto* bufferStart = readBuffer.data(); bufferStart < readBuffer.data() + bytesRead; /*Empty*/)
{
inotify_event* pEvent = (inotify_event*)bufferStart;
if (pEvent->mask & IN_CLOSE_WRITE)
{
std::string fileName((char*)pEvent->name);
std::cout << "File has been added: " << fileName << std::endl;
}
bufferStart += sizeof(inotify_event) + pEvent->len;
}
}
}
int main(int argc, char* argv[])
{
if (argc != 2)
{
std::cerr << "Incorrect number of arguments. Specify a directory to watch";
exit(EXIT_FAILURE);
}
int notifyFileDescriptor = inotify_init();
if (notifyFileDescriptor == -1)
{
perror("inotify_init");
exit(EXIT_FAILURE);
}
int watchDescriptor = inotify_add_watch(notifyFileDescriptor, argv[1], IN_CLOSE);
if (watchDescriptor == -1)
{
perror("inotify_add_watch");
exit(EXIT_FAILURE);
}
std::cout << "Beginning monitoring on " << argv[1] << std::endl;
MonitorDirectoryChange(notifyFileDescriptor);
std::cout << "Finished monitoring" << std::endl;
return EXIT_SUCCESS;
}
In this example program, the directory to watch is provided via the command line. If everything is successful, a watch for IN_CLOSE_WRITE events is added. These events are then monitored in a loop and the program outputs the name of files added to the directory when they are added. A screenshot of the execution is shown below, with a few files added to the target directory:
The next series of posts will cover the file watcher component of the system. This component will leverage the inotify API to pick up added source code files and begin the compilation and execution process.