I need to create a logger facility that outputs from different places of code to the same or different files depending on what the user provides. It should recreate a file for logging if it is not opened. But it must append to an already opened file.
This naive way such as
std::ofstream f1(“log”);
f1 << "1 from f1\n";
std::ofstream f2(“log”);
f2 << "1 from f2\n";
f1 << "2 from f1\n";
steals stream and recreates the file. Log contains
1 from f2
With append, it will reuse file, but the second open steals stream from f1
.
Log contains
1 from f1
1 from f2
Try to guess which files will be used and open them all in the very begging will work but may create a lot of files that are not actually used.
Open for append and closing on each logging call would be an almost working solution, but it seems to be a slow solution due to a lot of system calls and flushing on each logging action.
I’m going to create a static table of opened files, hoping that std::filesystem::canonical
will work in all of my cases. But as far as I understand such a table should already exist somewhere in the process.
I've read that in Fortran people can check if a file was opened using inquire
.
Check whether file has been opened already
But that answer did not give me any insight on how to achieve the same with С/C .
Update
A scratch of the logger with a "static" table of open logs can look like
//hpp
class Logger {
static std::mutex _mutex;
static std::unordered_map<std::string, std::ofstream> _openFiles;
std::ostream& _appender;
std::ostream& _createAppender(const std::filesystem::path& logPath);
public:
Logger(const std::filesystem::path& logPath):
_appender(_createAppender(logPath)) {
}
template<class... Args>
void log(const Args&... args) const {
std::scoped_lock<std::mutex> lock(_mutex);
(_appender << ... << args);
}
};
//cpp
#include "Logger.hpp"
std::mutex Logger::_mutex;
std::unordered_map<std::string, std::ofstream> Logger::_openFiles;
std::ostream& Logger::_createAppender(const std::filesystem::path& logPath) {
if (logPath.empty()) return std::cout;
const auto truePath{std::filesystem::weakly_canonical(logPath).string()};
std::scoped_lock<std::mutex> lock(_mutex);
const auto entry{_openFiles.find(truePath)};
if (entry != _openFiles.end()) return entry->second;
_openFiles.emplace(truePath, logPath);
std::ostream& stream{_openFiles[truePath]};
stream.exceptions(std::ifstream::failbit|std::ifstream::badbit);
return stream;
}
maybe it will help someone.
Yet, I still wonder if it is possible to get table mapping handles/descriptors from OS mentioned by @yzt, and will accept as an answer if someone explains how to do that inside the program.
CodePudding user response:
So here is a simple Linux specific code that checks whether a specified target file is open by the current process (using --std=c 17 for dir listing but any way can be used of course).
#include <string>
#include <iostream>
#include <filesystem>
#include <sys/types.h>
#include <unistd.h>
#include <limits.h>
bool is_open_by_me(const std::string &target)
{
char readlinkpath[PATH_MAX];
std::string path = "/proc/" std::to_string(getpid()) "/fd";
for (const auto & entry : std::filesystem::directory_iterator(path)) {
readlink(entry.path().c_str(), readlinkpath, sizeof(readlinkpath));
if (target == readlinkpath)
return true;
}
return false;
}
Simply list the current pid's open handles via proc, then use readlink function to resolve it to the actual file name.
That is the best way to do it from the userspace I know. This information is not known by the process itself, it is known by the kernel about the process, hence the process has to use various tricks, in this case parsing procfs, to access it.
If you want to check whether a different process hold an open handle to a file, you will have to parse all the procfs for all processes. That may not be always possible since other processes may be run by different users.
All that said - in your specific case, when you are the one owner, opening and closing the files - maintaining a table of open handles is a much cleaner solution.