diff --git a/io/io/CMakeLists.txt b/io/io/CMakeLists.txt index 6f30d2d9ac96a..d14937b2e6284 100644 --- a/io/io/CMakeLists.txt +++ b/io/io/CMakeLists.txt @@ -21,6 +21,7 @@ if (uring) endif () ROOT_LINKER_LIBRARY(RIO + src/InternalIOUtils.cxx src/RConcurrentHashColl.cxx src/RFile.cxx src/RRawFile.cxx @@ -66,6 +67,8 @@ ROOT_LINKER_LIBRARY(RIO ) target_include_directories(RIO PRIVATE ${CMAKE_SOURCE_DIR}/core/clib/res) +# PUBLIC so that dependent libraries e.g. ROOTDataFrame may find the internal header too +target_include_directories(RIO PUBLIC $) target_link_libraries(RIO PUBLIC ${ROOT_ATOMIC_LIBS}) target_link_libraries(RIO PRIVATE nlohmann_json::nlohmann_json) diff --git a/io/io/res/ROOT/InternalIOUtils.hxx b/io/io/res/ROOT/InternalIOUtils.hxx new file mode 100644 index 0000000000000..9c62c835d9e83 --- /dev/null +++ b/io/io/res/ROOT/InternalIOUtils.hxx @@ -0,0 +1,24 @@ +// Author: Vincenzo Eduardo Padulano (CERN), 08/2026 + +#ifndef ROOT_IO_UTILS +#define ROOT_IO_UTILS + +#include +#include +#include + +namespace ROOT::Internal { + +/// \brief Get extended attribute value from path +/// \param path Path to the file to check +/// \param xattr Extended attribute to evaluate +/// \return The string containing the extended attribute value if found, std::nullopt otherwise +std::optional GetXAttrVal(std::string_view path, std::string_view xattr); + +/// \brief Redirects the input path to the equivalent XRootD URL on EOS +/// \param inputUrl The input path to redirect +/// \return The redirected URL in case of successful redirection, std::nullopt otherwise +std::optional GetEOSRedirectedXRootURL(std::string_view inputPath); +} // namespace ROOT::Internal + +#endif diff --git a/io/io/src/InternalIOUtils.cxx b/io/io/src/InternalIOUtils.cxx new file mode 100644 index 0000000000000..f494d9461d456 --- /dev/null +++ b/io/io/src/InternalIOUtils.cxx @@ -0,0 +1,82 @@ +#include "ROOT/InternalIOUtils.hxx" + +#include "ROOT/RConfig.hxx" // R__UNIX + +#ifdef R__UNIX + +#include + +ROOT::RLogChannel &InternalLogChannel() +{ + static ROOT::RLogChannel sLog("ROOT.Internal"); + return sLog; +} + +// getxattr +#ifdef R__FBSD +#include +#else +#include +#endif + +#ifdef R__MACOSX +/* On macOS getxattr takes two extra arguments that should be set to 0 */ +#define getxattr(path, name, value, size) getxattr(path, name, value, size, 0u, 0) +#endif + +#ifdef R__FBSD +#define getxattr(path, name, value, size) extattr_get_file(path, EXTATTR_NAMESPACE_USER, name, value, size) +#endif + +#include "ROOT/StringUtils.hxx" // ROOT::StartsWith +#include "TEnv.h" // TEnv::GetValue +#endif + +std::optional +ROOT::Internal::GetXAttrVal([[maybe_unused]] std::string_view path, [[maybe_unused]] std::string_view xattr) +{ +#ifdef R__UNIX + // First call to getxattr evaluates the length of the extended attribute value + if (auto len = getxattr(path.data(), xattr.data(), nullptr, 0); len >= 0) { + std::string xval(len, 0); + // Second call extracts the extended attribute value, checking it's of the correct length + if (getxattr(path.data(), xattr.data(), xval.data(), len) == len) + return xval; + } +#endif + return std::nullopt; +} + +std::optional ROOT::Internal::GetEOSRedirectedXRootURL([[maybe_unused]] std::string_view inputPath) +{ +#ifdef R__UNIX + if (inputPath.empty() || inputPath.back() == '/') + return std::nullopt; + + if (gEnv->GetValue("TFile.CrossProtocolRedirects", 1) != 1) + return std::nullopt; + + auto xurl = ROOT::Internal::GetXAttrVal(inputPath, "eos.url.xroot"); + if (!xurl) + return std::nullopt; + + auto baseName = inputPath.substr(inputPath.find_last_of("/") + 1); + // Sometimes the `getxattr` call may return an invalid URL due + // to the POSIX attribute not being yet completely filled by EOS. + if (!std::equal(baseName.crbegin(), baseName.crend(), xurl->crbegin())) { + R__LOG_WARNING(InternalLogChannel()) + << "Could not find path base name '" << baseName << "' in redirected URL '" << *xurl << "'."; + return std::nullopt; + } + + // Ensure the redirected URL actually starts with the XRootD protocol string + if (ROOT::StartsWith(*xurl, "root://") || ROOT::StartsWith(*xurl, "xroot://") || + ROOT::StartsWith(*xurl, "roots://") || ROOT::StartsWith(*xurl, "xroots://")) + return xurl; + else + R__LOG_WARNING(InternalLogChannel()) + << "Redirected URL '" << *xurl << "' does not begin with any valid XRootD protocol string."; + +#endif + return std::nullopt; +} diff --git a/io/io/src/RRawFile.cxx b/io/io/src/RRawFile.cxx index 1304f6b8dda72..58d54daacf064 100644 --- a/io/io/src/RRawFile.cxx +++ b/io/io/src/RRawFile.cxx @@ -20,6 +20,7 @@ #include "TError.h" #include "TPluginManager.h" #include "TROOT.h" +#include "ROOT/InternalIOUtils.hxx" #include #include // for towlower @@ -68,6 +69,9 @@ ROOT::Internal::RRawFile::Create(std::string_view url, ROptions options) #ifdef _WIN32 return std::unique_ptr(new RRawFileWin(url, options)); #else + // We're assuming the input url is null-terminated in the next call + if (auto xurl = ROOT::Internal::GetEOSRedirectedXRootURL(url)) + return Create(*xurl, options); return std::unique_ptr(new RRawFileUnix(url, options)); #endif } diff --git a/io/io/src/TFile.cxx b/io/io/src/TFile.cxx index 993c3f0cb6ab6..d3450dc3ee791 100644 --- a/io/io/src/TFile.cxx +++ b/io/io/src/TFile.cxx @@ -171,6 +171,7 @@ The structure of a directory is shown in TDirectoryFile::TDirectoryFile #include "TThreadSlots.h" #include "TGlobal.h" #include "ROOT/RConcurrentHashColl.hxx" +#include "ROOT/InternalIOUtils.hxx" #include #include @@ -3811,34 +3812,16 @@ TFile *TFile::Open(const char *url, Option_t *options, const char *ftitle, TString expandedUrl(url); gSystem->ExpandPathName(expandedUrl); -#ifdef R__UNIX - // If URL is a file on an EOS FUSE mount, attempt redirection to XRootD protocol. - if (gEnv->GetValue("TFile.CrossProtocolRedirects", 1) == 1) { - TUrl fileurl(expandedUrl, /* default is file */ kTRUE); - if (strcmp(fileurl.GetProtocol(), "file") == 0) { - ssize_t len = getxattr(fileurl.GetFile(), "eos.url.xroot", nullptr, 0); - if (len > 0) { - std::string xurl(len, 0); - std::string fileNameFromUrl{fileurl.GetFile()}; - if (getxattr(fileNameFromUrl.c_str(), "eos.url.xroot", &xurl[0], len) == len) { - // Sometimes the `getxattr` call may return an invalid URL due - // to the POSIX attribute not being yet completely filled by EOS. - if (auto baseName = fileNameFromUrl.substr(fileNameFromUrl.find_last_of("/") + 1); - std::equal(baseName.crbegin(), baseName.crend(), xurl.crbegin())) { - if ((f = TFile::Open(xurl.c_str(), options, ftitle, compress, netopt))) { - if (!f->IsZombie()) { - return f; - } else { - delete f; - f = nullptr; - } - } - } - } + if (auto xurl = ROOT::Internal::GetEOSRedirectedXRootURL(expandedUrl)) { + if ((f = TFile::Open(xurl->c_str(), options, ftitle, compress, netopt))) { + if (!f->IsZombie()) { + return f; + } else { + delete f; + f = nullptr; } } } -#endif // If a timeout has been specified extract the value and try to apply it (it requires // support for asynchronous open, though; the following is completely transparent if diff --git a/tree/dataframe/src/RLoopManager.cxx b/tree/dataframe/src/RLoopManager.cxx index 5a4b36330d0c8..591b3790666e9 100644 --- a/tree/dataframe/src/RLoopManager.cxx +++ b/tree/dataframe/src/RLoopManager.cxx @@ -43,23 +43,8 @@ #include "ROOT/RSlotStack.hxx" #endif -#ifdef R__UNIX -// Functions needed to perform EOS XRootD redirection in ChangeSpec -#include "TEnv.h" +#include "ROOT/InternalIOUtils.hxx" #include "TSystem.h" -#ifndef R__FBSD -#include -#else -#include -#endif -#ifdef R__MACOSX -/* On macOS getxattr takes two extra arguments that should be set to 0 */ -#define getxattr(path, name, value, size) getxattr(path, name, value, size, 0u, 0) -#endif -#ifdef R__FBSD -#define getxattr(path, name, value, size) extattr_get_file(path, EXTATTR_NAMESPACE_USER, name, value, size) -#endif -#endif #include #include @@ -370,7 +355,6 @@ RLoopManager::RLoopManager(ROOT::RDF::Experimental::RDatasetSpec &&spec) ChangeSpec(std::move(spec)); } -#ifdef R__UNIX namespace { std::optional GetRedirectedSampleId(std::string_view path, std::string_view datasetName) { @@ -378,29 +362,14 @@ std::optional GetRedirectedSampleId(std::string_view path, std::str // If so, we create a redirected sample ID with the full xroot URL. TString expandedUrl(path.data()); gSystem->ExpandPathName(expandedUrl); - if (gEnv->GetValue("TFile.CrossProtocolRedirects", 1) == 1) { - TUrl fileurl(expandedUrl, /* default is file */ kTRUE); - if (strcmp(fileurl.GetProtocol(), "file") == 0) { - ssize_t len = getxattr(fileurl.GetFile(), "eos.url.xroot", nullptr, 0); - if (len > 0) { - std::string xurl(len, 0); - std::string fileNameFromUrl{fileurl.GetFile()}; - if (getxattr(fileNameFromUrl.c_str(), "eos.url.xroot", &xurl[0], len) == len) { - // Sometimes the `getxattr` call may return an invalid URL due - // to the POSIX attribute not being yet completely filled by EOS. - if (auto baseName = fileNameFromUrl.substr(fileNameFromUrl.find_last_of("/") + 1); - std::equal(baseName.crbegin(), baseName.crend(), xurl.crbegin())) { - return xurl + '/' + datasetName.data(); - } - } - } - } + TUrl fileurl(expandedUrl, /* default is file */ kTRUE); + if (strcmp(fileurl.GetProtocol(), "file") == 0) { + if (auto xurl = ROOT::Internal::GetEOSRedirectedXRootURL(fileurl.GetFile())) + return *xurl + '/' + datasetName.data(); } - return std::nullopt; } } // namespace -#endif /** * @brief Changes the internal TTree held by the RLoopManager. @@ -451,11 +420,10 @@ void RLoopManager::ChangeSpec(ROOT::RDF::Experimental::RDatasetSpec &&spec) // is exposed to users via RSampleInfo and DefinePerSample). const auto sampleId = files[i] + '/' + trees[i]; fSampleMap.insert({sampleId, &sample}); -#ifdef R__UNIX + // Also add redirected EOS xroot URL when available if (auto redirectedSampleId = GetRedirectedSampleId(files[i], trees[i])) fSampleMap.insert({redirectedSampleId.value(), &sample}); -#endif } } fDataSource = std::make_unique(std::move(chain), spec.GetFriendInfo()); @@ -473,11 +441,9 @@ void RLoopManager::ChangeSpec(ROOT::RDF::Experimental::RDatasetSpec &&spec) fileNames.push_back(files[i]); rntupleNames.insert(trees[i]); -#ifdef R__UNIX // Also add redirected EOS xroot URL when available if (auto redirectedSampleId = GetRedirectedSampleId(files[i], trees[i])) fSampleMap.insert({redirectedSampleId.value(), &sample}); -#endif } }