LCOV - code coverage report
Current view: top level - artdaq/ArtModules - RootDAQOutMF_module.cc (source / functions) Coverage Total Hit
Test: artdaq.info.cleaned Lines: 0.0 % 670 0
Test Date: 2026-10-29 00:35:58 Functions: 0.0 % 114 0

            Line data    Source code
       1              : // vim: set sw=2 expandtab :
       2              : #include "TRACE/tracemf.h"  // TLOG
       3              : #include "artdaq-utilities/Plugins/MetricData.hh"
       4              : #include "artdaq/DAQdata/Globals.hh"
       5              : #define TRACE_NAME (app_name + "_RootDAQOutMF").c_str()
       6              : 
       7              : #include "artdaq/ArtModules/ArtdaqSharedMemoryServiceInterface.h"
       8              : #include "artdaq/ArtModules/RootDAQOutFile.h"
       9              : 
      10              : #include "art/Framework/Core/ModuleMacros.h"
      11              : #include "art/Framework/Core/OutputModule.h"
      12              : #include "art/Framework/Core/RPManager.h"
      13              : #include "art/Framework/Core/ResultsProducer.h"
      14              : #include "art/Framework/IO/ClosingCriteria.h"
      15              : #include "art/Framework/IO/FileStatsCollector.h"
      16              : #include "art/Framework/IO/PostCloseFileRenamer.h"
      17              : #include "art/Framework/IO/detail/SafeFileNameConfig.h"
      18              : #include "art/Framework/IO/detail/logFileAction.h"
      19              : #include "art/Framework/IO/detail/validateFileNamePattern.h"
      20              : #include "art/Framework/Principal/EventPrincipal.h"
      21              : #include "art/Framework/Principal/ResultsPrincipal.h"
      22              : #include "art/Framework/Principal/RunPrincipal.h"
      23              : #include "art/Framework/Principal/SubRunPrincipal.h"
      24              : #include "art/Utilities/Globals.h"
      25              : #include "art/Utilities/parent_path.h"
      26              : #include "art/Utilities/unique_filename.h"
      27              : #include "art_root_io/DropMetaData.h"
      28              : #include "art_root_io/FastCloningEnabled.h"
      29              : #include "art_root_io/RootFileBlock.h"
      30              : #include "art_root_io/detail/rootOutputConfigurationTools.h"
      31              : #include "art_root_io/setup.h"
      32              : #include "canvas/Persistency/Provenance/ProductTables.h"
      33              : #include "canvas/Utilities/Exception.h"
      34              : #include "cetlib_except/exception.h"
      35              : #include "fhiclcpp/ParameterSet.h"
      36              : #include "fhiclcpp/types/Atom.h"
      37              : #include "fhiclcpp/types/ConfigurationTable.h"
      38              : #include "fhiclcpp/types/OptionalAtom.h"
      39              : #include "fhiclcpp/types/OptionalSequence.h"
      40              : #include "fhiclcpp/types/Table.h"
      41              : #include "fhiclcpp/types/TableFragment.h"
      42              : #include "messagefacility/MessageLogger/MessageLogger.h"
      43              : 
      44              : #include <fcntl.h>
      45              : #include <sys/file.h>
      46              : #include <unistd.h>
      47              : #include <algorithm>
      48              : #include <cerrno>
      49              : #include <cstring>
      50              : #include <deque>
      51              : #include <filesystem>
      52              : #include <iomanip>
      53              : #include <limits>
      54              : #include <map>
      55              : #include <memory>
      56              : #include <mutex>
      57              : #include <set>
      58              : #include <sstream>
      59              : #include <string>
      60              : #include <tuple>
      61              : #include <utility>
      62              : #include <vector>
      63              : 
      64              : using namespace std;
      65              : using namespace hep::concurrency;
      66              : 
      67              : namespace {
      68              : string const dev_null{"/dev/null"};
      69              : 
      70            0 : bool maxCriterionSpecified(art::ClosingCriteria const& cc)
      71              : {
      72            0 :         auto fp = mem_fn(&art::ClosingCriteria::fileProperties);
      73            0 :         return (fp(cc).nEvents() !=
      74            0 :                 art::ClosingCriteria::Defaults::unsigned_max()) ||
      75            0 :                (fp(cc).nSubRuns() !=
      76            0 :                 art::ClosingCriteria::Defaults::unsigned_max()) ||
      77            0 :                (fp(cc).nRuns() != art::ClosingCriteria::Defaults::unsigned_max()) ||
      78            0 :                (fp(cc).size() != art::ClosingCriteria::Defaults::size_max()) ||
      79            0 :                (fp(cc).age().count() !=
      80            0 :                 art::ClosingCriteria::Defaults::seconds_max());
      81              : }
      82              : 
      83            0 : auto shouldFastClone(bool const fastCloningSet,
      84              :                      bool const fastCloning,
      85              :                      bool const wantAllEvents,
      86              :                      art::ClosingCriteria const& cc)
      87              : {
      88            0 :         art::FastCloningEnabled enabled;
      89            0 :         if (fastCloningSet and not fastCloning)
      90              :         {
      91            0 :                 enabled.disable(
      92              :                     "RootDAQOutMF configuration explicitly disables fast cloning.");
      93            0 :                 return enabled;
      94              :         }
      95              : 
      96            0 :         if (not wantAllEvents)
      97              :         {
      98            0 :                 enabled.disable(
      99              :                     "Event-selection has been specified in the RootDAQOutMF configuration.");
     100              :         }
     101            0 :         if (fastCloning && maxCriterionSpecified(cc) &&
     102            0 :             cc.granularity() < art::Granularity::InputFile)
     103              :         {
     104            0 :                 enabled.disable(
     105              :                     "File-switching has been requested at event, subrun, or "
     106              :                     "run boundaries.");
     107              :         }
     108            0 :         return enabled;
     109            0 : }
     110              : 
     111              : struct SubrunStats
     112              : {
     113              :         size_t nEvents{0};
     114              :         art::EventNumber_t firstEvent{std::numeric_limits<art::EventNumber_t>::max()};
     115              :         art::EventNumber_t lastEvent{0};
     116              : };
     117              : 
     118            0 : static void writeSummaryFile(
     119              :     std::string const& summaryDir,
     120              :     std::map<art::SubRunID, SubrunStats> const& subrunStats,
     121              :     std::string const& closedFileName)
     122              : {
     123            0 :         if (summaryDir.empty() || subrunStats.empty()) { return; }
     124              : 
     125            0 :         std::string const outputFile = std::filesystem::path(closedFileName).filename().string();
     126              : 
     127              :         // Group rows by run number so that each run gets its own CSV file,
     128              :         // even when an output file spans multiple runs.
     129            0 :         std::map<art::RunNumber_t, std::ostringstream> runContents;
     130            0 :         for (auto const& [srid, stats] : subrunStats)
     131              :         {
     132            0 :                 runContents[srid.run()]
     133            0 :                     << srid.run()
     134            0 :                     << "," << srid.subRun()
     135            0 :                     << "," << stats.nEvents
     136            0 :                     << "," << stats.firstEvent
     137            0 :                     << "," << stats.lastEvent
     138              :                     << "," << outputFile
     139            0 :                     << "\n";
     140              :         }
     141              : 
     142              :         // One CSV file per run, appended — matches CFODataReceiver convention
     143            0 :         for (auto const& [run, contentStream] : runContents)
     144              :         {
     145            0 :                 std::ostringstream fname;
     146            0 :                 fname << summaryDir;
     147            0 :                 if (summaryDir.back() != '/') { fname << '/'; }
     148            0 :                 fname << "subrun_record_run" << std::setw(6) << std::setfill('0') << run << ".csv";
     149              : 
     150            0 :                 int fd = open(fname.str().c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
     151            0 :                 if (fd < 0)
     152              :                 {
     153            0 :                         TLOG(TLVL_WARNING) << "writeSummaryFile: could not open \"" << fname.str() << "\" for writing: " << strerror(errno);
     154            0 :                         continue;
     155            0 :                 }
     156              : 
     157            0 :                 flock(fd, LOCK_EX);
     158              : 
     159            0 :                 std::string const rows = contentStream.str();
     160            0 :                 std::string buf;
     161            0 :                 if (lseek(fd, 0, SEEK_END) == 0)
     162              :                 {
     163            0 :                         buf = "run,subrun,n_events,first_event,last_event,output_file\n";
     164              :                 }
     165            0 :                 buf += rows;
     166              : 
     167              :                 // Loop to handle partial writes and EINTR
     168            0 :                 if (buf.size() > static_cast<size_t>(std::numeric_limits<ssize_t>::max()))
     169              :                 {
     170            0 :                         TLOG(TLVL_ERROR) << "writeSummaryFile: buffer too large (" << buf.size() << " bytes) to write to \""
     171            0 :                                          << fname.str() << "\"";
     172              :                 }
     173              :                 else
     174              :                 {
     175            0 :                         ssize_t total = 0;
     176            0 :                         auto remaining = static_cast<ssize_t>(buf.size());
     177            0 :                         while (remaining > 0)
     178              :                         {
     179            0 :                                 ssize_t written = ::write(fd, buf.c_str() + total, static_cast<size_t>(remaining));
     180            0 :                                 if (written < 0)
     181              :                                 {
     182            0 :                                         if (errno == EINTR) { continue; }
     183            0 :                                         TLOG(TLVL_ERROR) << "writeSummaryFile: write error to \"" << fname.str() << "\": " << strerror(errno);
     184            0 :                                         break;
     185              :                                 }
     186            0 :                                 total += written;
     187            0 :                                 remaining -= written;
     188              :                         }
     189              :                 }
     190              : 
     191            0 :                 flock(fd, LOCK_UN);
     192            0 :                 close(fd);
     193              : 
     194            0 :                 size_t const rowsWritten = static_cast<size_t>(std::count(rows.begin(), rows.end(), '\n'));
     195            0 :                 TLOG(TLVL_DEBUG) << "writeSummaryFile: appended " << rowsWritten
     196            0 :                                  << " subrun row(s) to \"" << fname.str() << "\"";
     197            0 :         }
     198            0 : }
     199              : }  // namespace
     200              : 
     201              : namespace art {
     202              : 
     203              : // RootDAQOutMF is a variant of RootDAQOut that can keep multiple ROOT files
     204              : // open at the same time.  When the active file's closing criteria are met (e.g.
     205              : // maxEvents, maxSubRuns, maxRuns), it is moved to a "pending-close" queue and a
     206              : // new file is opened immediately.  Files in the pending-close queue still have
     207              : // their TFile open in memory; they are flushed to disk only when the queue
     208              : // would exceed maxOpenFiles.  This pipelining reduces the gap in data writing
     209              : // that occurs during file transitions.
     210              : class RootDAQOutMF final : public OutputModule
     211              : {
     212              : public:
     213              :         static constexpr char const* default_tmpDir{"<parent-path-of-filename>"};
     214              : 
     215              :         struct Config
     216              :         {
     217              :                 using Name = fhicl::Name;
     218              :                 using Comment = fhicl::Comment;
     219              :                 template<typename T>
     220              :                 using Atom = fhicl::Atom<T>;
     221              :                 template<typename T>
     222              :                 using OptionalAtom = fhicl::OptionalAtom<T>;
     223              :                 fhicl::TableFragment<OutputModule::Config> omConfig;
     224              :                 Atom<string> catalog{Name("catalog"), ""};
     225              :                 OptionalAtom<bool> dropAllEvents{Name("dropAllEvents")};
     226              :                 Atom<bool> dropAllSubRuns{Name("dropAllSubRuns"), false};
     227              :                 OptionalAtom<bool> fastCloning{Name("fastCloning")};
     228              :                 Atom<string> tmpDir{Name("tmpDir"), default_tmpDir};
     229            0 :                 Atom<int> compressionLevel{Name("compressionLevel"), 501};
     230            0 :                 Atom<unsigned> freePercent{Name("freePercent"), 0};
     231            0 :                 Atom<unsigned> freeMB{Name("freeMB"), 0};
     232              :                 Atom<int64_t> saveMemoryObjectThreshold{Name("saveMemoryObjectThreshold"),
     233            0 :                                                         -1l};
     234            0 :                 Atom<int64_t> treeMaxVirtualSize{Name("treeMaxVirtualSize"), -1};
     235            0 :                 Atom<int> splitLevel{Name("splitLevel"), 0};
     236            0 :                 Atom<int> basketSize{Name("basketSize"), 65536};
     237              :                 Atom<bool> dropMetaDataForDroppedData{Name("dropMetaDataForDroppedData"),
     238              :                                                       false};
     239              :                 Atom<string> dropMetaData{Name("dropMetaData"), "NONE"};
     240              :                 Atom<bool> writeParameterSets{Name("writeParameterSets"), true};
     241              :                 fhicl::Table<ClosingCriteria::Config> fileProperties{
     242              :                     Name("fileProperties"),
     243              :                     Comment("The 'fileProperties' parameter is specified to enable "
     244              :                             "output-file switching.")};
     245              :                 fhicl::TableFragment<detail::SafeFileNameConfig> safeFileName;
     246            0 :                 Atom<int> firstLoggerRank{Name("firstLoggerRank"), -1};
     247              :                 Atom<unsigned> maxOpenFiles{
     248              :                     Name("maxOpenFiles"),
     249              :                     Comment("Maximum number of ROOT files that can be open simultaneously.\n"
     250              :                             "When this limit is reached, the oldest pending file is flushed\n"
     251              :                             "to disk before a new file is opened.  A value of 1 gives the\n"
     252              :                             "same behavior as RootDAQOut (no pipelining).  Higher values\n"
     253              :                             "allow TFile::Close() of older files to overlap with writing\n"
     254              :                             "new events to the current file."),
     255            0 :                     5u};
     256              : 
     257              :                 struct NewSubStringForApp
     258              :                 {
     259              :                         fhicl::Atom<string> appName{fhicl::Name("appName")};
     260              :                         fhicl::Atom<string> newString{fhicl::Name("newString")};
     261              :                 };
     262              :                 struct FileNameSubstitution
     263              :                 {
     264              :                         fhicl::Atom<string> targetString{fhicl::Name("targetString")};
     265              :                         fhicl::Sequence<fhicl::Table<NewSubStringForApp>> replacementList{fhicl::Name("replacementList")};
     266              :                 };
     267              :                 fhicl::OptionalSequence<fhicl::Table<FileNameSubstitution>> fileNameSubstitutions{Name("fileNameSubstitutions")};
     268              :                 Atom<string> summaryDir{Name("subrunRecordDir"), Comment("Directory for per-file CSV subrun record (subrun/event statistics). Empty = disabled."), ""};
     269              : 
     270            0 :                 Config()
     271            0 :                 {
     272              :                         // Both RootDAQOutMF module and OutputModule use the "fileName"
     273              :                         // FHiCL parameter.  However, whereas in OutputModule the
     274              :                         // parameter has a default, for RootDAQOutMF the parameter should
     275              :                         // not.  We therefore have to change the default flag setting
     276              :                         // for 'OutputModule::Config::fileName'.
     277              :                         using namespace fhicl::detail;
     278              :                         ParameterBase* adjustFilename{
     279            0 :                             const_cast<fhicl::Atom<string>*>(&omConfig().fileName)};  // NOLINT(cppcoreguidelines-pro-type-const-cast)
     280            0 :                         adjustFilename->set_par_style(fhicl::par_style::REQUIRED);
     281            0 :                 }
     282              : 
     283              :                 struct KeysToIgnore
     284              :                 {
     285              :                         set<string>
     286            0 :                         operator()() const
     287              :                         {
     288            0 :                                 set<string> keys{OutputModule::Config::KeysToIgnore::get()};
     289            0 :                                 keys.insert("results");
     290            0 :                                 return keys;
     291            0 :                         }
     292              :                 };
     293              :         };
     294              : 
     295              :         using Parameters = fhicl::WrappedTable<Config, Config::KeysToIgnore>;
     296              : 
     297              :         ~RootDAQOutMF() override;
     298              :         explicit RootDAQOutMF(Parameters const&);
     299              :         RootDAQOutMF(RootDAQOutMF const&) = delete;
     300              :         RootDAQOutMF(RootDAQOutMF&&) = delete;
     301              :         RootDAQOutMF& operator=(RootDAQOutMF const&) = delete;
     302              :         RootDAQOutMF& operator=(RootDAQOutMF&&) = delete;
     303              : 
     304              :         void postSelectProducts() override;
     305              :         void beginJob() override;
     306              :         void endJob() override;
     307              :         void beginRun(RunPrincipal const&) override;
     308              :         void endRun(RunPrincipal const&) override;
     309              :         void beginSubRun(SubRunPrincipal const&) override;
     310              :         void endSubRun(SubRunPrincipal const&) override;
     311              :         void event(EventPrincipal const&) override;
     312              : 
     313              : private:
     314              :         // Replace OutputModule Functions.
     315              :         string fileNameAtOpen() const;
     316              :         string fileNameAtClose(PostCloseFileRenamer& renamer,
     317              :                                string const& currentFileName);
     318              :         string const& lastClosedFileName() const override;
     319              :         Granularity fileGranularity() const override;
     320              :         void openFile(FileBlock const&) override;
     321              :         void respondToOpenInputFile(FileBlock const&) override;
     322              :         void readResults(ResultsPrincipal const& resp) override;
     323              :         void respondToCloseInputFile(FileBlock const&) override;
     324              :         void incrementInputFileNumber() override;
     325              :         void write(EventPrincipal&) override;
     326              :         void writeSubRun(SubRunPrincipal&) override;
     327              :         void writeRun(RunPrincipal&) override;
     328              :         void setSubRunAuxiliaryRangeSetID(RangeSet const&) override;
     329              :         void setRunAuxiliaryRangeSetID(RangeSet const&) override;
     330              :         bool isFileOpen() const override;
     331              :         void setFileStatus(OutputFileStatus) override;
     332              :         bool requestsToCloseFile() const override;
     333              :         void startEndFile() override;
     334              :         void writeFileFormatVersion() override;
     335              :         void writeFileIndex() override;
     336              :         void writeProcessConfigurationRegistry() override;
     337              :         void writeProcessHistoryRegistry() override;
     338              :         void writeParameterSetRegistry() override;
     339              :         void writeProductDescriptionRegistry() override;
     340              :         void writeParentageRegistry() override;
     341              :         void doWriteFileCatalogMetadata(
     342              :             FileCatalogMetadata::collection_type const& md,
     343              :             FileCatalogMetadata::collection_type const& ssmd) override;
     344              :         void writeProductDependencies() override;
     345              :         void finishEndFile() override;
     346              :         void doRegisterProducts(ProductDescriptions& productsToProduce,
     347              :                                 ModuleDescription const& md) override;
     348              :         std::string modifyFilePattern(std::string const& inputPattern,
     349              :                                       Config const& config);
     350              : 
     351              :         // Per-file state bundle (non-copyable, non-movable -- stored via unique_ptr
     352              :         // so the PostCloseFileRenamer reference into fstats remains stable).
     353              :         struct OutputFileBundle
     354              :         {
     355              :                 FileStatsCollector fstats;
     356              :                 PostCloseFileRenamer fRenamer;
     357              :                 std::unique_ptr<RootDAQOutFile> file{nullptr};
     358              :                 std::string tmpFileName{};
     359              :                 bool metadataNeedsRefresh{false};
     360              :                 std::string closedFileName{};
     361              :                 std::map<art::SubRunID, SubrunStats> subrunStats;
     362              :                 std::set<art::SubRunID> writtenSubRuns;
     363              :                 std::set<art::RunID> writtenRuns;
     364              : 
     365            0 :                 OutputFileBundle(std::string const& moduleLabel,
     366              :                                  std::string const& processName)
     367            0 :                     : fstats(moduleLabel, processName), fRenamer(fstats)
     368            0 :                 {}
     369              : 
     370              :                 OutputFileBundle(OutputFileBundle const&) = delete;
     371              :                 OutputFileBundle(OutputFileBundle&&) = delete;
     372              :                 OutputFileBundle& operator=(OutputFileBundle const&) = delete;
     373              :                 OutputFileBundle& operator=(OutputFileBundle&&) = delete;
     374              :         };
     375              : 
     376              :         // Implementation Details.
     377              :         void doOpenFile();
     378              :         void closePendingFile(std::unique_ptr<OutputFileBundle>& bundle);
     379              :         void closeOldestPendingFileIfNeeded();
     380              :         void removeBundleMappings(OutputFileBundle* bundle);
     381              :         void markLateWrite(OutputFileBundle* bundle);
     382              :         OutputFileBundle* targetBundleForEvent(EventPrincipal const& ep);
     383              :         OutputFileBundle* targetBundleForSubRun(SubRunPrincipal const& sr);
     384              :         OutputFileBundle* targetBundleForRun(RunPrincipal const& rp);
     385              : 
     386              :         using RoutingKey = std::tuple<unsigned, unsigned, unsigned>;
     387              :         using SubRunIDKey = std::pair<unsigned, unsigned>;
     388              :         unsigned bucketIndex(unsigned idValue, unsigned maxPerFile) const;
     389              :         RoutingKey makeRoutingKey(EventPrincipal const& ep) const;
     390              :         RoutingKey makeRoutingKey(SubRunPrincipal const& sr) const;
     391              :         RoutingKey makeRoutingKey(RunPrincipal const& rp) const;
     392              :         static SubRunIDKey makeSubRunIDKey(EventPrincipal const& ep);
     393              :         static SubRunIDKey makeSubRunIDKey(SubRunPrincipal const& sr);
     394              :         static unsigned makeRunIDKey(SubRunPrincipal const& sr);
     395              :         static unsigned makeRunIDKey(RunPrincipal const& rp);
     396              : 
     397              :         // Data Members.
     398              :         mutable std::recursive_mutex mutex_;
     399              :         string const catalog_;
     400              :         bool dropAllEvents_{false};
     401              :         bool dropAllSubRuns_;
     402              :         string const moduleLabel_;
     403              :         int inputFileCount_{};
     404              : 
     405              :         // Multi-file state.
     406              :         // activeFile_: the file currently receiving events.
     407              :         // pendingFiles_: files whose writeTTrees() has been called but whose
     408              :         //                TFile has not yet been closed (destructed).
     409              :         std::unique_ptr<OutputFileBundle> activeFile_{nullptr};
     410              :         std::deque<std::unique_ptr<OutputFileBundle>> pendingFiles_;
     411              :         unsigned const maxOpenFiles_;
     412              :         std::map<RoutingKey, OutputFileBundle*> routeToBundle_;
     413              :         std::map<SubRunIDKey, std::set<OutputFileBundle*>> subRunToBundles_;
     414              :         std::map<unsigned, std::set<OutputFileBundle*>> runToBundles_;
     415              :         FileCatalogMetadata::collection_type lastFileCatalogMetadata_{};
     416              :         FileCatalogMetadata::collection_type lastSubRunMetadata_{};
     417              :         bool hasCatalogMetadata_{false};
     418              : 
     419              :         string const filePattern_;
     420              :         string tmpDir_;
     421              :         string lastClosedFileName_{};
     422              :         int const compressionLevel_;
     423              :         unsigned freePercent_;
     424              :         unsigned freeMB_;
     425              :         int64_t const saveMemoryObjectThreshold_;
     426              :         int64_t const treeMaxVirtualSize_;
     427              :         int const splitLevel_;
     428              :         int const basketSize_;
     429              :         DropMetaData dropMetaData_;
     430              :         bool dropMetaDataForDroppedData_;
     431              :         FastCloningEnabled fastCloningEnabled_{};
     432              :         // Set false only for cases where we are guaranteed never to need historical
     433              :         // ParameterSet information in the downstream file, such as when mixing.
     434              :         bool writeParameterSets_;
     435              :         ClosingCriteria fileProperties_;
     436              :         string summaryDir_;
     437              :         size_t filesOpenedInRun_{0};
     438              :         size_t filesClosedInRun_{0};
     439              :         // Shared %# sequence counter across all OutputFileBundle instances.
     440              :         // Key = intermediate filename with sentinel in place of the index.
     441              :         std::map<std::string, size_t> sharedFileIndex_;
     442              :         ProductDescriptions productsToProduce_{};
     443              :         ProductTables producedResultsProducts_{ProductTables::invalid()};
     444              :         RPManager rpm_;
     445              : };
     446              : 
     447            0 : RootDAQOutMF::~RootDAQOutMF()
     448              : {
     449            0 :         TLOG(TLVL_INFO) << "RootDAQOutMF DESTRUCTOR this=" << static_cast<void const*>(this)
     450            0 :                         << " label=" << moduleLabel_
     451            0 :                         << " filesOpenedInRun_=" << filesOpenedInRun_
     452            0 :                         << " filesClosedInRun_=" << filesClosedInRun_
     453            0 :                         << " activeFile=" << (activeFile_ ? "non-null" : "null")
     454            0 :                         << " pendingFiles=" << pendingFiles_.size();
     455              : 
     456            0 :         if (activeFile_)
     457              :         {
     458            0 :                 TLOG(TLVL_WARNING) << "RootDAQOutMF DESTRUCTOR: active output file still present at destruction. "
     459            0 :                                    << "This indicates shutdown before normal end-file finalization.";
     460              :         }
     461              : 
     462              :         // Best-effort fallback: if endJob is bypassed but destruction proceeds
     463              :         // normally, close any pending files so metadata/trees are flushed.
     464              :         // This must never throw from a destructor.
     465              :         try
     466              :         {
     467            0 :                 std::lock_guard sentry{mutex_};
     468            0 :                 while (!pendingFiles_.empty())
     469              :                 {
     470            0 :                         closePendingFile(pendingFiles_.front());
     471            0 :                         pendingFiles_.pop_front();
     472              :                 }
     473            0 :         }
     474            0 :         catch (cet::exception const& ex)
     475              :         {
     476            0 :                 TLOG(TLVL_ERROR) << "RootDAQOutMF DESTRUCTOR: exception while closing pending files: "
     477            0 :                                  << ex.what();
     478            0 :         }
     479            0 :         catch (std::exception const& ex)
     480              :         {
     481            0 :                 TLOG(TLVL_ERROR) << "RootDAQOutMF DESTRUCTOR: std::exception while closing pending files: "
     482            0 :                                  << ex.what();
     483            0 :         }
     484            0 :         catch (...)
     485              :         {
     486            0 :                 TLOG(TLVL_ERROR) << "RootDAQOutMF DESTRUCTOR: unknown exception while closing pending files";
     487            0 :         }
     488            0 : }
     489              : 
     490            0 : RootDAQOutMF::RootDAQOutMF(Parameters const& config)
     491              :     : OutputModule{
     492            0 :           config().omConfig}
     493            0 :     , catalog_{config().catalog()}
     494            0 :     , dropAllSubRuns_{config().dropAllSubRuns()}
     495            0 :     , moduleLabel_{config.get_PSet().get<string>("module_label")}
     496            0 :     , maxOpenFiles_{config().maxOpenFiles()}
     497            0 :     , filePattern_{modifyFilePattern(config().omConfig().fileName(), config())}
     498            0 :     , tmpDir_{config().tmpDir() == default_tmpDir ? parent_path(filePattern_) : config().tmpDir()}
     499            0 :     , compressionLevel_{config().compressionLevel()}
     500            0 :     , freePercent_{config().freePercent()}
     501            0 :     , freeMB_{config().freeMB()}
     502            0 :     , saveMemoryObjectThreshold_{config().saveMemoryObjectThreshold()}
     503            0 :     , treeMaxVirtualSize_{config().treeMaxVirtualSize()}
     504            0 :     , splitLevel_{config().splitLevel()}
     505            0 :     , basketSize_{config().basketSize()}
     506            0 :     , dropMetaData_{config().dropMetaData()}
     507            0 :     , dropMetaDataForDroppedData_{config().dropMetaDataForDroppedData()}
     508            0 :     , writeParameterSets_{config().writeParameterSets()}
     509            0 :     , fileProperties_{config().fileProperties()}
     510            0 :     , summaryDir_{config().summaryDir()}
     511            0 :     , rpm_{config.get_PSet()}
     512              : {
     513            0 :         TLOG(TLVL_INFO) << "RootDAQOutMF_module (s124 version) CONSTRUCTOR Start this=" << static_cast<void const*>(this)
     514            0 :                         << " label=" << moduleLabel_;
     515              : 
     516            0 :         if (maxOpenFiles_ == 0)
     517              :         {
     518            0 :                 throw Exception(errors::Configuration)  // NOLINT(cert-err60-cpp)
     519            0 :                     << "RootDAQOutMF: maxOpenFiles must be >= 1.\n";
     520              :         }
     521              : 
     522            0 :         bool const check_filename = config.get_PSet().has_key("fileProperties") and
     523            0 :                                     config().safeFileName().checkFileName();
     524            0 :         detail::validateFileNamePattern(check_filename, filePattern_);
     525              : 
     526              :         // Setup the streamers and error handlers.
     527            0 :         root::setup();
     528              : 
     529            0 :         bool const dropAllEventsSet{config().dropAllEvents(dropAllEvents_)};
     530            0 :         dropAllEvents_ = detail::shouldDropEvents(
     531            0 :             dropAllEventsSet, dropAllEvents_, dropAllSubRuns_);
     532              :         // N.B. Any time file switching is enabled at a boundary other than
     533              :         //      InputFile, fastCloningEnabled_ ***MUST*** be deactivated.  This is
     534              :         //      to ensure that the Event tree from the InputFile is not
     535              :         //      accidentally cloned to the output file before the output
     536              :         //      module has seen the events that are going to be processed.
     537            0 :         bool fastCloningEnabled{true};
     538            0 :         bool const fastCloningSet{config().fastCloning(fastCloningEnabled)};
     539            0 :         fastCloningEnabled_ = shouldFastClone(
     540            0 :             fastCloningSet, fastCloningEnabled, wantAllEvents(), fileProperties_);
     541              : 
     542            0 :         if (auto const n = Globals::instance()->nschedules(); n > 1)
     543              :         {
     544            0 :                 std::ostringstream oss;
     545            0 :                 oss << "More than one schedule (" << n << ") is being used.";
     546            0 :                 fastCloningEnabled_.disable(oss.str());
     547            0 :         }
     548              : 
     549            0 :         if (!writeParameterSets_)
     550              :         {
     551            0 :                 mf::LogWarning("PROVENANCE")
     552            0 :                     << "Output module " << moduleLabel_
     553            0 :                     << " has parameter writeParameterSets set to false.\n"
     554              :                     << "Parameter set provenance will not be available in subsequent "
     555            0 :                        "jobs.\n"
     556              :                     << "Check your experiment's policy on this issue to avoid future "
     557            0 :                        "problems\n"
     558            0 :                     << "with analysis reproducibility.\n";
     559              :         }
     560            0 : }
     561              : 
     562            0 : void RootDAQOutMF::openFile(FileBlock const& fb)
     563              : {
     564            0 :         std::lock_guard sentry{mutex_};
     565            0 :         TLOG(TLVL_DEBUG) << __func__ << ": entered, isFileOpen=" << isFileOpen()
     566            0 :                          << ", pendingFiles=" << pendingFiles_.size()
     567            0 :                          << ", filesOpenedInRun_=" << filesOpenedInRun_;
     568              :         // Note: The file block here refers to the currently open
     569              :         //       input file, so we can find out about the available
     570              :         //       products by looping over the branches of the input
     571              :         //       file data trees.
     572            0 :         if (!isFileOpen())
     573              :         {
     574              :                 // Close oldest pending file if opening a new one would exceed the limit.
     575            0 :                 closeOldestPendingFileIfNeeded();
     576            0 :                 doOpenFile();
     577            0 :                 respondToOpenInputFile(fb);
     578              :         }
     579              :         else
     580              :         {
     581            0 :                 TLOG(TLVL_DEBUG) << __func__ << ": skipped doOpenFile because file already open";
     582              :         }
     583            0 : }
     584              : 
     585            0 : void RootDAQOutMF::postSelectProducts()
     586              : {
     587            0 :         std::lock_guard sentry{mutex_};
     588            0 :         if (isFileOpen())
     589              :         {
     590            0 :                 activeFile_->file->selectProducts();
     591              :         }
     592            0 : }
     593              : 
     594            0 : void RootDAQOutMF::respondToOpenInputFile(FileBlock const& fb)
     595              : {
     596            0 :         std::lock_guard sentry{mutex_};
     597            0 :         ++inputFileCount_;
     598            0 :         if (!isFileOpen())
     599              :         {
     600            0 :                 return;
     601              :         }
     602            0 :         auto const* rfb = dynamic_cast<RootFileBlock const*>(&fb);
     603            0 :         auto fastCloneThisOne = fastCloningEnabled_;
     604            0 :         if (!rfb)
     605              :         {
     606            0 :                 fastCloneThisOne.disable("Input source does not read art/ROOT files.");
     607              :         }
     608              :         else
     609              :         {
     610            0 :                 fastCloneThisOne.merge(rfb->fastClonable());
     611              :         }
     612            0 :         activeFile_->file->beginInputFile(rfb, std::move(fastCloneThisOne));
     613            0 :         activeFile_->fstats.recordInputFile(fb.fileName());
     614            0 : }
     615              : 
     616            0 : void RootDAQOutMF::readResults(ResultsPrincipal const& resp)
     617              : {
     618            0 :         std::lock_guard sentry{mutex_};
     619            0 :         rpm_.for_each_RPWorker(
     620            0 :             [&resp](RPWorker& w) { w.rp().doReadResults(resp); });
     621            0 : }
     622              : 
     623            0 : void RootDAQOutMF::respondToCloseInputFile(FileBlock const& fb)
     624              : {
     625            0 :         std::lock_guard sentry{mutex_};
     626            0 :         if (isFileOpen())
     627              :         {
     628            0 :                 activeFile_->file->respondToCloseInputFile(fb);
     629              :         }
     630            0 : }
     631              : 
     632            0 : void RootDAQOutMF::write(EventPrincipal& ep)
     633              : {
     634            0 :         std::lock_guard sentry{mutex_};
     635            0 :         if (dropAllEvents_)
     636              :         {
     637            0 :                 return;
     638              :         }
     639            0 :         if (hasNewlyDroppedBranch()[InEvent])
     640              :         {
     641            0 :                 ep.addToProcessHistory();
     642            0 :                 ep.refreshProcessHistoryID();
     643              :         }
     644            0 :         auto* bundle = targetBundleForEvent(ep);
     645            0 :         bundle->file->writeOne(ep);
     646            0 :         bundle->fstats.recordEvent(ep.eventID());
     647            0 :         auto const& eid = ep.eventID();
     648            0 :         auto& sr = bundle->subrunStats[eid.subRunID()];
     649            0 :         ++sr.nEvents;
     650            0 :         if (eid.event() < sr.firstEvent) { sr.firstEvent = eid.event(); }
     651            0 :         if (eid.event() > sr.lastEvent) { sr.lastEvent = eid.event(); }
     652            0 :         routeToBundle_[makeRoutingKey(ep)] = bundle;
     653            0 :         subRunToBundles_[makeSubRunIDKey(ep)].insert(bundle);
     654            0 : }
     655              : 
     656            0 : void RootDAQOutMF::setSubRunAuxiliaryRangeSetID(RangeSet const& rs)
     657              : {
     658            0 :         std::lock_guard sentry{mutex_};
     659            0 :         if (activeFile_)
     660              :         {
     661            0 :                 activeFile_->file->setSubRunAuxiliaryRangeSetID(rs);
     662              :         }
     663            0 : }
     664              : 
     665            0 : void RootDAQOutMF::writeSubRun(SubRunPrincipal& sr)
     666              : {
     667            0 :         std::lock_guard sentry{mutex_};
     668            0 :         if (dropAllSubRuns_)
     669              :         {
     670            0 :                 return;
     671              :         }
     672            0 :         if (hasNewlyDroppedBranch()[InSubRun])
     673              :         {
     674            0 :                 sr.addToProcessHistory();
     675              :         }
     676            0 :         auto* const targetBundle = targetBundleForSubRun(sr);
     677            0 :         std::set<OutputFileBundle*> bundlesToWrite{targetBundle};
     678            0 :         if (auto const it = subRunToBundles_.find(makeSubRunIDKey(sr));
     679            0 :             it != subRunToBundles_.end())
     680              :         {
     681            0 :                 bundlesToWrite.insert(it->second.begin(), it->second.end());
     682              :         }
     683            0 :         for (auto* bundle : bundlesToWrite)
     684              :         {
     685            0 :                 if (!bundle->writtenSubRuns.insert(sr.subRunID()).second)
     686            0 :                         continue;
     687            0 :                 markLateWrite(bundle);
     688            0 :                 bundle->file->writeSubRun(sr);
     689            0 :                 bundle->fstats.recordSubRun(sr.subRunID());
     690            0 :                 runToBundles_[makeRunIDKey(sr)].insert(bundle);
     691              :         }
     692            0 :         routeToBundle_[makeRoutingKey(sr)] = targetBundle;
     693            0 : }
     694              : 
     695            0 : void RootDAQOutMF::setRunAuxiliaryRangeSetID(RangeSet const& rs)
     696              : {
     697            0 :         std::lock_guard sentry{mutex_};
     698            0 :         if (activeFile_)
     699              :         {
     700            0 :                 activeFile_->file->setRunAuxiliaryRangeSetID(rs);
     701              :         }
     702            0 : }
     703              : 
     704            0 : void RootDAQOutMF::writeRun(RunPrincipal& rp)
     705              : {
     706            0 :         std::lock_guard sentry{mutex_};
     707            0 :         if (hasNewlyDroppedBranch()[InRun])
     708              :         {
     709            0 :                 rp.addToProcessHistory();
     710              :         }
     711            0 :         auto* const targetBundle = targetBundleForRun(rp);
     712            0 :         std::set<OutputFileBundle*> bundlesToWrite{targetBundle};
     713            0 :         if (auto const it = runToBundles_.find(makeRunIDKey(rp));
     714            0 :             it != runToBundles_.end())
     715              :         {
     716            0 :                 bundlesToWrite.insert(it->second.begin(), it->second.end());
     717              :         }
     718            0 :         for (auto* bundle : bundlesToWrite)
     719              :         {
     720            0 :                 if (!bundle->writtenRuns.insert(rp.runID()).second)
     721            0 :                         continue;
     722            0 :                 markLateWrite(bundle);
     723            0 :                 bundle->file->writeRun(rp);
     724            0 :                 bundle->fstats.recordRun(rp.runID());
     725              :         }
     726            0 :         routeToBundle_[makeRoutingKey(rp)] = targetBundle;
     727            0 : }
     728              : 
     729            0 : void RootDAQOutMF::startEndFile()
     730              : {
     731            0 :         std::lock_guard sentry{mutex_};
     732              :         auto resp = make_unique<ResultsPrincipal>(
     733            0 :             ResultsAuxiliary{}, moduleDescription().processConfiguration(), nullptr);
     734            0 :         resp->createGroupsForProducedProducts(producedResultsProducts_);
     735            0 :         resp->enableLookupOfProducedProducts();
     736            0 :         if (!producedResultsProducts_.descriptions(InResults).empty() ||
     737            0 :             hasNewlyDroppedBranch()[InResults])
     738              :         {
     739            0 :                 resp->addToProcessHistory();
     740              :         }
     741            0 :         rpm_.for_each_RPWorker(
     742            0 :             [&resp](RPWorker& w) { w.rp().doWriteResults(*resp); });
     743            0 :         activeFile_->file->writeResults(*resp);
     744            0 : }
     745              : 
     746            0 : void RootDAQOutMF::writeFileFormatVersion()
     747              : {
     748            0 :         std::lock_guard sentry{mutex_};
     749            0 :         activeFile_->file->writeFileFormatVersion();
     750            0 : }
     751              : 
     752            0 : void RootDAQOutMF::writeFileIndex()
     753              : {
     754            0 :         std::lock_guard sentry{mutex_};
     755              :         // Intentionally deferred for RootDAQOutMF:
     756              :         // events may still be routed to a pending file after finishEndFile().
     757              :         // Writing the index here can therefore miss late events.  We write the
     758              :         // FileIndex immediately before writeTTrees() when the pending file is
     759              :         // actually being closed in closePendingFile().
     760            0 : }
     761              : 
     762            0 : void RootDAQOutMF::writeProcessConfigurationRegistry()
     763              : {
     764            0 :         std::lock_guard sentry{mutex_};
     765            0 :         activeFile_->file->writeProcessConfigurationRegistry();
     766            0 : }
     767              : 
     768            0 : void RootDAQOutMF::writeProcessHistoryRegistry()
     769              : {
     770            0 :         std::lock_guard sentry{mutex_};
     771              :         // Intentionally deferred for RootDAQOutMF:
     772              :         // events may still be routed to a pending file after finishEndFile().
     773              :         // Writing ProcessHistory here can therefore miss entries needed by late
     774              :         // events. We write ProcessHistory immediately before writeTTrees() when the
     775              :         // pending file is actually being closed in closePendingFile().
     776            0 : }
     777              : 
     778            0 : void RootDAQOutMF::writeParameterSetRegistry()
     779              : {
     780            0 :         std::lock_guard sentry{mutex_};
     781            0 :         if (writeParameterSets_)
     782              :         {
     783            0 :                 activeFile_->file->writeParameterSetRegistry();
     784              :         }
     785            0 : }
     786              : 
     787            0 : void RootDAQOutMF::writeProductDescriptionRegistry()
     788              : {
     789            0 :         std::lock_guard sentry{mutex_};
     790            0 :         activeFile_->file->writeProductDescriptionRegistry();
     791            0 : }
     792              : 
     793            0 : void RootDAQOutMF::writeParentageRegistry()
     794              : {
     795            0 :         std::lock_guard sentry{mutex_};
     796            0 :         activeFile_->file->writeParentageRegistry();
     797            0 : }
     798              : 
     799            0 : void RootDAQOutMF::doWriteFileCatalogMetadata(
     800              :     FileCatalogMetadata::collection_type const& md,
     801              :     FileCatalogMetadata::collection_type const& ssmd)
     802              : {
     803            0 :         std::lock_guard sentry{mutex_};
     804            0 :         lastFileCatalogMetadata_ = md;
     805            0 :         lastSubRunMetadata_ = ssmd;
     806            0 :         hasCatalogMetadata_ = true;
     807            0 :         activeFile_->metadataNeedsRefresh = false;
     808            0 :         activeFile_->file->writeFileCatalogMetadata(activeFile_->fstats, md, ssmd);
     809            0 : }
     810              : 
     811            0 : void RootDAQOutMF::writeProductDependencies()
     812              : {
     813            0 :         std::lock_guard sentry{mutex_};
     814            0 :         activeFile_->file->writeProductDependencies();
     815            0 : }
     816              : 
     817            0 : void RootDAQOutMF::finishEndFile()
     818              : {
     819            0 :         std::lock_guard sentry{mutex_};
     820            0 :         TLOG(TLVL_DEBUG) << __func__ << ": entered, isFileOpen=" << (activeFile_ != nullptr)
     821            0 :                          << ", pendingFiles before move=" << pendingFiles_.size()
     822            0 :                          << ", filesClosedInRun_=" << filesClosedInRun_;
     823            0 :         string const tmpFileName{activeFile_->file->currentFileName()};
     824              : 
     825              :         // Record the close before computing the final name so the %# sequence
     826              :         // advances the same way it does in RootDAQOut.
     827            0 :         activeFile_->fstats.recordFileClose();
     828              : 
     829              :         // Determine the final output file name now, but defer the actual rename
     830              :         // until the file has been fully closed.
     831            0 :         activeFile_->tmpFileName = tmpFileName;
     832            0 :         lastClosedFileName_ = (filePattern_ == dev_null)
     833            0 :                                   ? dev_null
     834            0 :                                   : activeFile_->fRenamer.applySubstitutions(
     835            0 :                                         filePattern_);
     836              : 
     837            0 :         activeFile_->closedFileName = lastClosedFileName_;
     838            0 :         TLOG(TLVL_INFO) << __func__ << ": Queued output file \"" << lastClosedFileName_
     839            0 :                         << "\" for deferred TFile::Close() (pendingFiles will have "
     840            0 :                         << pendingFiles_.size() + 1 << " entries)";
     841              : 
     842              :         // Move the active bundle to the pending queue.  The TFile is still open
     843              :         // in memory; it will be closed (flushed to disk) when closePendingFile()
     844              :         // is called.
     845            0 :         pendingFiles_.push_back(std::move(activeFile_));
     846            0 :         activeFile_.reset();  // Mark as "no active file" from art's perspective.
     847              : 
     848            0 :         rpm_.invoke(&ResultsProducer::doClear);
     849            0 : }
     850              : 
     851            0 : void RootDAQOutMF::doRegisterProducts(ProductDescriptions& producedProducts,
     852              :                                       ModuleDescription const& md)
     853              : {
     854            0 :         std::lock_guard sentry{mutex_};
     855              :         // Register Results products from ResultsProducers.
     856            0 :         rpm_.for_each_RPWorker([&producedProducts, &md](RPWorker& w) {
     857            0 :                 auto const& params = w.params();
     858            0 :                 w.setModuleDescription(
     859            0 :                     ModuleDescription{params.rpPSetID,
     860            0 :                                       params.rpPluginType,
     861            0 :                                       md.moduleLabel() + '#' + params.rpLabel,
     862              :                                       ModuleThreadingType::legacy,
     863              :                                       md.processConfiguration()});
     864            0 :                 w.rp().registerProducts(producedProducts, w.moduleDescription());
     865            0 :         });
     866              :         // Form product table for Results products.  We do this here so we
     867              :         // can appropriately set the product tables for the ResultsPrincipal.
     868            0 :         productsToProduce_ = producedProducts;
     869            0 :         producedResultsProducts_ = ProductTables{productsToProduce_};
     870            0 : }
     871              : 
     872            0 : void RootDAQOutMF::setFileStatus(OutputFileStatus const ofs)
     873              : {
     874            0 :         std::lock_guard sentry{mutex_};
     875            0 :         if (isFileOpen())
     876              :         {
     877            0 :                 activeFile_->file->setFileStatus(ofs);
     878              :         }
     879            0 : }
     880              : 
     881            0 : bool RootDAQOutMF::isFileOpen() const
     882              : {
     883            0 :         std::lock_guard sentry{mutex_};
     884            0 :         return activeFile_ != nullptr;
     885            0 : }
     886              : 
     887            0 : void RootDAQOutMF::incrementInputFileNumber()
     888              : {
     889            0 :         std::lock_guard sentry{mutex_};
     890            0 :         if (isFileOpen())
     891              :         {
     892            0 :                 activeFile_->file->incrementInputFileNumber();
     893              :         }
     894            0 : }
     895              : 
     896            0 : bool RootDAQOutMF::requestsToCloseFile() const
     897              : {
     898            0 :         std::lock_guard sentry{mutex_};
     899            0 :         return isFileOpen() ? activeFile_->file->requestsToCloseFile() : false;
     900            0 : }
     901              : 
     902              : Granularity
     903            0 : RootDAQOutMF::fileGranularity() const
     904              : {
     905            0 :         std::lock_guard sentry{mutex_};
     906            0 :         return fileProperties_.granularity();
     907            0 : }
     908              : 
     909            0 : void RootDAQOutMF::doOpenFile()
     910              : {
     911            0 :         std::lock_guard sentry{mutex_};
     912            0 :         if (inputFileCount_ == 0)
     913              :         {
     914            0 :                 throw Exception(errors::LogicError)  // NOLINT(cert-err60-cpp)
     915              :                     << "Attempt to open output file before input file. "
     916            0 :                     << "Please report this to the core framework developers.\n";
     917              :         }
     918            0 :         activeFile_ = std::make_unique<OutputFileBundle>(moduleLabel_, processName());
     919            0 :         activeFile_->file = make_unique<RootDAQOutFile>(this,
     920            0 :                                                         fileNameAtOpen(),
     921            0 :                                                         fileProperties_,
     922            0 :                                                         compressionLevel_,
     923            0 :                                                         freePercent_,
     924            0 :                                                         freeMB_,
     925            0 :                                                         saveMemoryObjectThreshold_,
     926            0 :                                                         treeMaxVirtualSize_,
     927            0 :                                                         splitLevel_,
     928            0 :                                                         basketSize_,
     929            0 :                                                         dropMetaData_,
     930            0 :                                                         dropMetaDataForDroppedData_);
     931            0 :         activeFile_->fstats.recordFileOpen();
     932            0 :         ++filesOpenedInRun_;
     933            0 :         TLOG(TLVL_DEBUG) << __func__ << ": filesOpenedInRun_ now " << filesOpenedInRun_
     934            0 :                          << ", metricMan=" << (metricMan ? "non-null" : "NULL");
     935            0 :         if (metricMan)
     936              :         {
     937            0 :                 metricMan->sendMetric("Output Files Opened", filesOpenedInRun_, "files", 3, artdaq::MetricMode::LastPoint | artdaq::MetricMode::Persist);
     938              :         }
     939            0 :         TLOG(TLVL_INFO) << __func__ << ": Opened output file with pattern \"" << filePattern_ << "\"";
     940            0 : }
     941              : 
     942            0 : void RootDAQOutMF::closePendingFile(std::unique_ptr<OutputFileBundle>& bundle)
     943              : {
     944            0 :         removeBundleMappings(bundle.get());
     945            0 :         if (bundle->metadataNeedsRefresh && hasCatalogMetadata_)
     946              :         {
     947            0 :                 bundle->file->writeFileCatalogMetadata(
     948            0 :                     bundle->fstats, lastFileCatalogMetadata_, lastSubRunMetadata_);
     949              :         }
     950              :         // ProcessHistory must be written at true close time so it captures all
     951              :         // history information needed by late-routed events.
     952            0 :         bundle->file->writeProcessHistoryRegistry();
     953              :         // FileIndex must be written at true close time so it captures all events
     954              :         // that may have been routed to this file while it was pending.
     955            0 :         bundle->file->writeFileIndex();
     956            0 :         bundle->file->writeTTrees();
     957              :         // Destroying the RootDAQOutFile calls TFile::Close(), which flushes the
     958              :         // ROOT key directory and closes the file descriptor.
     959            0 :         TLOG(TLVL_INFO) << __func__ << ": Closing pending file (TFile::Close)";
     960            0 :         bundle->file.reset();
     961            0 :         bundle->closedFileName =
     962            0 :             fileNameAtClose(bundle->fRenamer, bundle->tmpFileName);
     963            0 :         writeSummaryFile(summaryDir_, bundle->subrunStats, bundle->closedFileName);
     964            0 :         ++filesClosedInRun_;
     965            0 :         TLOG(TLVL_DEBUG) << __func__ << ": filesClosedInRun_ now " << filesClosedInRun_
     966            0 :                          << ", metricMan=" << (metricMan ? "non-null" : "NULL");
     967            0 :         if (metricMan)
     968              :         {
     969            0 :                 metricMan->sendMetric("Last Closed Output File", bundle->closedFileName, "", 3, artdaq::MetricMode::LastPoint);
     970            0 :                 metricMan->sendMetric("Output Files Closed", filesClosedInRun_, "files", 3, artdaq::MetricMode::LastPoint | artdaq::MetricMode::Persist);
     971              :         }
     972            0 :         bundle.reset();
     973            0 : }
     974              : 
     975            0 : void RootDAQOutMF::closeOldestPendingFileIfNeeded()
     976              : {
     977            0 :         std::lock_guard sentry{mutex_};
     978              :         // Total open files after opening a new active = pendingFiles_.size() + 1.
     979              :         // Close oldest pending entries until we are within the limit.
     980            0 :         while (!pendingFiles_.empty() &&
     981            0 :                (pendingFiles_.size() + 1 > maxOpenFiles_))
     982              :         {
     983            0 :                 TLOG(TLVL_INFO) << __func__
     984            0 :                                 << ": Closing oldest pending file to respect maxOpenFiles="
     985            0 :                                 << maxOpenFiles_
     986            0 :                                 << " (currently pending=" << pendingFiles_.size() << ")";
     987            0 :                 closePendingFile(pendingFiles_.front());
     988            0 :                 pendingFiles_.pop_front();
     989              :         }
     990            0 : }
     991              : 
     992            0 : void RootDAQOutMF::removeBundleMappings(OutputFileBundle* bundle)
     993              : {
     994            0 :         for (auto it = routeToBundle_.begin(); it != routeToBundle_.end();)
     995              :         {
     996            0 :                 if (it->second == bundle)
     997              :                 {
     998            0 :                         it = routeToBundle_.erase(it);
     999              :                 }
    1000              :                 else
    1001              :                 {
    1002            0 :                         ++it;
    1003              :                 }
    1004              :         }
    1005            0 :         for (auto it = subRunToBundles_.begin(); it != subRunToBundles_.end();)
    1006              :         {
    1007            0 :                 it->second.erase(bundle);
    1008            0 :                 if (it->second.empty())
    1009              :                 {
    1010            0 :                         it = subRunToBundles_.erase(it);
    1011              :                 }
    1012              :                 else
    1013              :                 {
    1014            0 :                         ++it;
    1015              :                 }
    1016              :         }
    1017            0 :         for (auto it = runToBundles_.begin(); it != runToBundles_.end();)
    1018              :         {
    1019            0 :                 it->second.erase(bundle);
    1020            0 :                 if (it->second.empty())
    1021              :                 {
    1022            0 :                         it = runToBundles_.erase(it);
    1023              :                 }
    1024              :                 else
    1025              :                 {
    1026            0 :                         ++it;
    1027              :                 }
    1028              :         }
    1029            0 : }
    1030              : 
    1031            0 : void RootDAQOutMF::markLateWrite(OutputFileBundle* bundle)
    1032              : {
    1033            0 :         if (bundle != activeFile_.get())
    1034              :         {
    1035            0 :                 bundle->metadataNeedsRefresh = true;
    1036              :         }
    1037            0 : }
    1038              : 
    1039              : RootDAQOutMF::OutputFileBundle*
    1040            0 : RootDAQOutMF::targetBundleForEvent(EventPrincipal const& ep)
    1041              : {
    1042            0 :         auto* bundle = activeFile_.get();
    1043            0 :         if (auto const it = routeToBundle_.find(makeRoutingKey(ep));
    1044            0 :             it != routeToBundle_.end())
    1045              :         {
    1046            0 :                 bundle = it->second;
    1047              :         }
    1048            0 :         markLateWrite(bundle);
    1049            0 :         return bundle;
    1050              : }
    1051              : 
    1052              : RootDAQOutMF::OutputFileBundle*
    1053            0 : RootDAQOutMF::targetBundleForSubRun(SubRunPrincipal const& sr)
    1054              : {
    1055            0 :         auto* bundle = activeFile_.get();
    1056            0 :         if (auto const it = routeToBundle_.find(makeRoutingKey(sr));
    1057            0 :             it != routeToBundle_.end())
    1058              :         {
    1059            0 :                 bundle = it->second;
    1060              :         }
    1061            0 :         markLateWrite(bundle);
    1062            0 :         return bundle;
    1063              : }
    1064              : 
    1065              : RootDAQOutMF::OutputFileBundle*
    1066            0 : RootDAQOutMF::targetBundleForRun(RunPrincipal const& rp)
    1067              : {
    1068            0 :         auto* bundle = activeFile_.get();
    1069            0 :         if (auto const it = routeToBundle_.find(makeRoutingKey(rp));
    1070            0 :             it != routeToBundle_.end())
    1071              :         {
    1072            0 :                 bundle = it->second;
    1073              :         }
    1074            0 :         markLateWrite(bundle);
    1075            0 :         return bundle;
    1076              : }
    1077              : 
    1078              : unsigned
    1079            0 : RootDAQOutMF::bucketIndex(unsigned const idValue, unsigned const maxPerFile) const
    1080              : {
    1081            0 :         return (maxPerFile == ClosingCriteria::Defaults::unsigned_max() ||
    1082            0 :                 maxPerFile == 0u || idValue == 0u)
    1083            0 :                    ? 0u
    1084            0 :                    : (idValue - 1u) / maxPerFile;
    1085              : }
    1086              : 
    1087              : RootDAQOutMF::RoutingKey
    1088            0 : RootDAQOutMF::makeRoutingKey(EventPrincipal const& ep) const
    1089              : {
    1090            0 :         auto const limits = fileProperties_.fileProperties();
    1091            0 :         auto const id = ep.eventID();
    1092            0 :         return {bucketIndex(static_cast<unsigned>(id.run()), limits.nRuns()),
    1093            0 :                 bucketIndex(static_cast<unsigned>(id.subRun()), limits.nSubRuns()),
    1094            0 :                 bucketIndex(static_cast<unsigned>(id.event()), limits.nEvents())};
    1095            0 : }
    1096              : 
    1097              : RootDAQOutMF::RoutingKey
    1098            0 : RootDAQOutMF::makeRoutingKey(SubRunPrincipal const& sr) const
    1099              : {
    1100            0 :         auto const limits = fileProperties_.fileProperties();
    1101            0 :         auto const id = sr.subRunID();
    1102            0 :         return {bucketIndex(static_cast<unsigned>(id.run()), limits.nRuns()),
    1103            0 :                 bucketIndex(static_cast<unsigned>(id.subRun()), limits.nSubRuns()),
    1104            0 :                 0u};
    1105            0 : }
    1106              : 
    1107              : RootDAQOutMF::RoutingKey
    1108            0 : RootDAQOutMF::makeRoutingKey(RunPrincipal const& rp) const
    1109              : {
    1110            0 :         auto const limits = fileProperties_.fileProperties();
    1111            0 :         return {bucketIndex(static_cast<unsigned>(rp.runID().run()),
    1112              :                             limits.nRuns()),
    1113            0 :                 0u,
    1114            0 :                 0u};
    1115            0 : }
    1116              : 
    1117              : RootDAQOutMF::SubRunIDKey
    1118            0 : RootDAQOutMF::makeSubRunIDKey(EventPrincipal const& ep)
    1119              : {
    1120            0 :         auto const id = ep.eventID();
    1121            0 :         return {static_cast<unsigned>(id.run()), static_cast<unsigned>(id.subRun())};
    1122              : }
    1123              : 
    1124              : RootDAQOutMF::SubRunIDKey
    1125            0 : RootDAQOutMF::makeSubRunIDKey(SubRunPrincipal const& sr)
    1126              : {
    1127            0 :         auto const id = sr.subRunID();
    1128            0 :         return {static_cast<unsigned>(id.run()), static_cast<unsigned>(id.subRun())};
    1129              : }
    1130              : 
    1131              : unsigned
    1132            0 : RootDAQOutMF::makeRunIDKey(SubRunPrincipal const& sr)
    1133              : {
    1134            0 :         return static_cast<unsigned>(sr.subRunID().run());
    1135              : }
    1136              : 
    1137              : unsigned
    1138            0 : RootDAQOutMF::makeRunIDKey(RunPrincipal const& rp)
    1139              : {
    1140            0 :         return static_cast<unsigned>(rp.runID().run());
    1141              : }
    1142              : 
    1143              : string
    1144            0 : RootDAQOutMF::fileNameAtOpen() const
    1145              : {
    1146            0 :         return (filePattern_ == dev_null) ? dev_null : unique_filename(tmpDir_ + "/RootDAQOutMF");
    1147              : }
    1148              : 
    1149              : string
    1150            0 : RootDAQOutMF::fileNameAtClose(PostCloseFileRenamer& renamer,
    1151              :                               string const& currentFileName)
    1152              : {
    1153            0 :         if (filePattern_ == dev_null) return dev_null;
    1154              : 
    1155              :         // We need a shared %# index counter across all OutputFileBundle instances,
    1156              :         // because multiple bundles may write files with the same %r/%s values
    1157              :         // (same subrun), causing independent per-bundle counters to produce the
    1158              :         // same destination filename.
    1159              :         //
    1160              :         // Strategy:
    1161              :         //  1. Replace "%N#" in the pattern with a sentinel literal that the renamer
    1162              :         //     will pass through unchanged (it only substitutes %X tokens).
    1163              :         //  2. Let the bundle's renamer resolve %r, %s, timestamps, etc., producing
    1164              :         //     an intermediate filename that contains the sentinel.
    1165              :         //  3. Use that intermediate name as the key into the module-level shared
    1166              :         //     counter, increment it, then rename to the final destination.
    1167            0 :         static const boost::regex kSeqRe{R"(%([0-9]*)#)"};
    1168            0 :         static const std::string kSentinel{"__SEQIDX__"};
    1169              : 
    1170            0 :         bool const hasSeq = boost::regex_search(filePattern_, kSeqRe);
    1171            0 :         if (!hasSeq)
    1172              :         {
    1173              :                 // No %# in pattern — delegate directly to the bundle's renamer.
    1174            0 :                 return renamer.maybeRenameFile(currentFileName, filePattern_);
    1175              :         }
    1176              : 
    1177              :         // Extract optional width from the first %N# occurrence (default 3).
    1178            0 :         unsigned width = 3;
    1179              :         {
    1180            0 :                 boost::smatch m;
    1181            0 :                 if (boost::regex_search(filePattern_, m, boost::regex{R"(%([0-9]+)#)"}))
    1182              :                 {
    1183            0 :                         width = static_cast<unsigned>(std::stoul(m[1].str()));
    1184              :                 }
    1185            0 :         }
    1186              : 
    1187              :         // Build a modified pattern with %N# replaced by the sentinel literal.
    1188              :         std::string const sentinelPattern =
    1189            0 :             boost::regex_replace(filePattern_, kSeqRe, kSentinel);
    1190              : 
    1191              :         // Rename tmpFile → intermediate path (sentinel still present in name).
    1192              :         // This resolves %r, %s, timestamps, etc. via the bundle's renamer.
    1193              :         std::string const intermediateName =
    1194            0 :             renamer.maybeRenameFile(currentFileName, sentinelPattern);
    1195              : 
    1196              :         // Assign the next unique index for this base name across all bundles.
    1197            0 :         size_t const idx = ++sharedFileIndex_[intermediateName];
    1198            0 :         if (idx > 1)
    1199              :         {
    1200            0 :                 TLOG(TLVL_INFO) << __func__ << ": shared %# index=" << idx
    1201            0 :                                 << " for base pattern \"" << intermediateName
    1202            0 :                                 << "\" (multiple bundles had the same run/subrun)";
    1203              :         }
    1204              : 
    1205              :         // Build final name by replacing the sentinel with the formatted index.
    1206            0 :         std::ostringstream oss;
    1207            0 :         oss << std::setfill('0') << std::setw(width) << idx;
    1208              :         std::string const finalName =
    1209            0 :             boost::regex_replace(intermediateName, boost::regex{kSentinel}, oss.str());
    1210              : 
    1211              :         // Rename intermediate → final (single atomic rename on same filesystem).
    1212            0 :         std::error_code ec;
    1213            0 :         std::filesystem::rename(intermediateName, finalName, ec);
    1214            0 :         if (ec)
    1215              :         {
    1216              :                 // Cross-filesystem fallback: copy then remove.
    1217            0 :                 std::filesystem::copy_file(intermediateName, finalName,
    1218              :                                            std::filesystem::copy_options::overwrite_existing);
    1219            0 :                 std::filesystem::remove(intermediateName);
    1220              :         }
    1221            0 :         return finalName;
    1222            0 : }
    1223              : 
    1224              : string const&
    1225            0 : RootDAQOutMF::lastClosedFileName() const
    1226              : {
    1227            0 :         std::lock_guard sentry{mutex_};
    1228            0 :         if (lastClosedFileName_.empty())
    1229              :         {
    1230            0 :                 throw Exception(errors::LogicError, "RootDAQOutMF::lastClosedFileName(): ")  // NOLINT(cert-err60-cpp)
    1231            0 :                     << "called before meaningful.\n";
    1232              :         }
    1233            0 :         return lastClosedFileName_;
    1234            0 : }
    1235              : 
    1236            0 : void RootDAQOutMF::beginJob()
    1237              : {
    1238            0 :         std::lock_guard sentry{mutex_};
    1239            0 :         rpm_.invoke(&ResultsProducer::doBeginJob);
    1240            0 : }
    1241              : 
    1242            0 : void RootDAQOutMF::endJob()
    1243              : {
    1244            0 :         std::lock_guard sentry{mutex_};
    1245              :         // Close any files still in the pending queue (TFile::Close not yet called).
    1246            0 :         TLOG(TLVL_INFO) << __func__ << ": Closing " << pendingFiles_.size()
    1247            0 :                         << " pending file(s) at end of job";
    1248            0 :         while (!pendingFiles_.empty())
    1249              :         {
    1250            0 :                 closePendingFile(pendingFiles_.front());
    1251            0 :                 pendingFiles_.pop_front();
    1252              :         }
    1253            0 :         rpm_.invoke(&ResultsProducer::doEndJob);
    1254            0 : }
    1255              : 
    1256            0 : void RootDAQOutMF::event(EventPrincipal const& ep)
    1257              : {
    1258            0 :         std::lock_guard sentry{mutex_};
    1259            0 :         rpm_.for_each_RPWorker([&ep](RPWorker& w) { w.rp().doEvent(ep); });
    1260            0 : }
    1261              : 
    1262            0 : void RootDAQOutMF::beginSubRun(SubRunPrincipal const& srp)
    1263              : {
    1264            0 :         std::lock_guard sentry{mutex_};
    1265            0 :         rpm_.for_each_RPWorker([&srp](RPWorker& w) { w.rp().doBeginSubRun(srp); });
    1266            0 : }
    1267              : 
    1268            0 : void RootDAQOutMF::endSubRun(SubRunPrincipal const& srp)
    1269              : {
    1270            0 :         std::lock_guard sentry{mutex_};
    1271            0 :         rpm_.for_each_RPWorker([&srp](RPWorker& w) { w.rp().doEndSubRun(srp); });
    1272            0 : }
    1273              : 
    1274            0 : void RootDAQOutMF::beginRun(RunPrincipal const& rp)
    1275              : {
    1276            0 :         std::lock_guard sentry{mutex_};
    1277            0 :         rpm_.for_each_RPWorker([&rp](RPWorker& w) { w.rp().doBeginRun(rp); });
    1278            0 : }
    1279              : 
    1280            0 : void RootDAQOutMF::endRun(RunPrincipal const& rp)
    1281              : {
    1282            0 :         std::lock_guard sentry{mutex_};
    1283            0 :         rpm_.for_each_RPWorker([&rp](RPWorker& w) { w.rp().doEndRun(rp); });
    1284            0 : }
    1285              : 
    1286              : std::string
    1287            0 : RootDAQOutMF::modifyFilePattern(std::string const& inputPattern, Config const& config)
    1288              : {
    1289              :         // Make sure that the shared memory is connected
    1290            0 :         art::ServiceHandle<ArtdaqSharedMemoryServiceInterface> shm;
    1291              : 
    1292            0 :         TLOG(TLVL_DEBUG + 32) << __func__ << ": inputPattern=\"" << inputPattern << "\"";
    1293              : 
    1294              :         // fetch the firstLoggerRank and fileNameSubstitutions (if provided) for use in
    1295              :         // substituting keywords in the filename pattern
    1296            0 :         int firstLoggerRank = config.firstLoggerRank();
    1297            0 :         std::vector<Config::FileNameSubstitution> subs;
    1298            0 :         config.fileNameSubstitutions(subs);
    1299            0 :         TLOG(TLVL_DEBUG + 33) << __func__ << ": firstLoggerRank=" << firstLoggerRank
    1300            0 :                               << ", numberOfSubstitutionsProvided=" << subs.size();
    1301              : 
    1302              :         // initialization
    1303            0 :         std::string modifiedPattern = inputPattern;
    1304            0 :         std::string searchString;
    1305              :         size_t targetLocation;
    1306            0 :         int zeroBasedRelativeRank = my_rank;
    1307            0 :         int oneBasedRelativeRank = my_rank + 1;
    1308            0 :         if (firstLoggerRank >= 0)
    1309              :         {
    1310            0 :                 zeroBasedRelativeRank -= firstLoggerRank;
    1311            0 :                 oneBasedRelativeRank -= firstLoggerRank;
    1312              :         }
    1313            0 :         TLOG(TLVL_DEBUG + 33) << __func__ << ": my_rank=" << my_rank << ", zeroBasedRelativeRank=" << zeroBasedRelativeRank
    1314            0 :                               << ", oneBasedRelativeRank=" << oneBasedRelativeRank;
    1315              : 
    1316              :         // if the "ZeroBasedRelativeRank" keyword was specified in the filename pattern,
    1317              :         // perform the substitution
    1318            0 :         searchString = "${ZeroBasedRelativeRank}";
    1319            0 :         targetLocation = modifiedPattern.find(searchString);
    1320            0 :         TLOG(TLVL_DEBUG + 33) << __func__ << ":" << __LINE__ << " searchString=" << searchString << ", targetLocation=" << targetLocation;
    1321            0 :         while (targetLocation != std::string::npos)
    1322              :         {
    1323            0 :                 std::ostringstream oss;
    1324            0 :                 oss << zeroBasedRelativeRank;
    1325            0 :                 modifiedPattern.replace(targetLocation, searchString.length(), oss.str());
    1326            0 :                 targetLocation = modifiedPattern.find(searchString);
    1327            0 :                 TLOG(TLVL_DEBUG + 33) << __func__ << ":" << __LINE__ << " searchString=" << searchString << ", targetLocation=" << targetLocation;
    1328            0 :         }
    1329              : 
    1330              :         // if the "OneBasedRelativeRank" keyword was specified in the filename pattern,
    1331              :         // perform the substitution
    1332            0 :         searchString = "${OneBasedRelativeRank}";
    1333            0 :         targetLocation = modifiedPattern.find(searchString);
    1334            0 :         TLOG(TLVL_DEBUG + 33) << __func__ << ":" << __LINE__ << " searchString=" << searchString << ", targetLocation=" << targetLocation;
    1335            0 :         while (targetLocation != std::string::npos)
    1336              :         {
    1337            0 :                 std::ostringstream oss;
    1338            0 :                 oss << oneBasedRelativeRank;
    1339            0 :                 modifiedPattern.replace(targetLocation, searchString.length(), oss.str());
    1340            0 :                 targetLocation = modifiedPattern.find(searchString);
    1341            0 :                 TLOG(TLVL_DEBUG + 33) << __func__ << ":" << __LINE__ << " searchString=" << searchString << ", targetLocation=" << targetLocation;
    1342            0 :         }
    1343              : 
    1344              :         // if the "Rank" keyword was specified in the filename pattern,
    1345              :         // perform the substitution
    1346            0 :         searchString = "${Rank}";
    1347            0 :         targetLocation = modifiedPattern.find(searchString);
    1348            0 :         TLOG(TLVL_DEBUG + 33) << __func__ << ":" << __LINE__ << " searchString=" << searchString << ", targetLocation=" << targetLocation;
    1349            0 :         while (targetLocation != std::string::npos)
    1350              :         {
    1351            0 :                 std::ostringstream oss;
    1352            0 :                 oss << my_rank;
    1353            0 :                 modifiedPattern.replace(targetLocation, searchString.length(), oss.str());
    1354            0 :                 targetLocation = modifiedPattern.find(searchString);
    1355            0 :                 TLOG(TLVL_DEBUG + 33) << __func__ << ":" << __LINE__ << " searchString=" << searchString << ", targetLocation=" << targetLocation;
    1356            0 :         }
    1357              : 
    1358              :         // if the "app_name" keyword was specified in the filename pattern,
    1359              :         // perform the substitution
    1360            0 :         searchString = "${app_name}";
    1361            0 :         targetLocation = modifiedPattern.find(searchString);
    1362            0 :         TLOG(TLVL_DEBUG + 33) << __func__ << ":" << __LINE__ << " searchString=" << searchString << ", targetLocation=" << targetLocation;
    1363            0 :         while (targetLocation != std::string::npos)
    1364              :         {
    1365            0 :                 std::ostringstream oss;
    1366            0 :                 oss << artdaq::Globals::app_name_;
    1367            0 :                 modifiedPattern.replace(targetLocation, searchString.length(), oss.str());
    1368            0 :                 targetLocation = modifiedPattern.find(searchString);
    1369            0 :                 TLOG(TLVL_DEBUG + 33) << __func__ << ":" << __LINE__ << " searchString=" << searchString << ", targetLocation=" << targetLocation;
    1370            0 :         }
    1371              : 
    1372              :         // if one or more free-form substitutions were provided, we'll do them here
    1373            0 :         for (auto& sub : subs)
    1374              :         {
    1375              :                 // first look up the replacement string for this process's app_name
    1376            0 :                 const std::string BLAH = "none_provided";
    1377            0 :                 std::string newString = BLAH;
    1378            0 :                 std::vector<Config::NewSubStringForApp> replacementList = sub.replacementList();
    1379            0 :                 for (auto& rdx : replacementList)
    1380              :                 {
    1381            0 :                         if (rdx.appName() == artdaq::Globals::app_name_)
    1382              :                         {
    1383            0 :                                 newString = rdx.newString();
    1384            0 :                                 break;
    1385              :                         }
    1386              :                 }
    1387            0 :                 TLOG(TLVL_DEBUG + 33) << __func__ << ": app_name=" << artdaq::Globals::app_name_ << ", newString=" << newString;
    1388            0 :                 if (newString != BLAH)
    1389              :                 {
    1390              :                         // first, add the expected surrounding text, and search for that
    1391            0 :                         searchString = "${" + sub.targetString() + "}";
    1392            0 :                         targetLocation = modifiedPattern.find(searchString);
    1393            0 :                         TLOG(TLVL_DEBUG + 33) << __func__ << ":" << __LINE__ << " searchString=" << searchString << ", targetLocation=" << targetLocation;
    1394            0 :                         while (targetLocation != std::string::npos)
    1395              :                         {
    1396            0 :                                 modifiedPattern.replace(targetLocation, searchString.length(), newString);
    1397            0 :                                 targetLocation = modifiedPattern.find(searchString);
    1398            0 :                                 TLOG(TLVL_DEBUG + 33) << __func__ << ":" << __LINE__ << " searchString=" << searchString << ", targetLocation=" << targetLocation;
    1399              :                         }
    1400              : 
    1401              :                         // then, search for the provided string, verbatim, in case the user specified
    1402              :                         // the enclosing text in the configuration document
    1403            0 :                         searchString = sub.targetString();
    1404            0 :                         targetLocation = modifiedPattern.find(searchString);
    1405            0 :                         TLOG(TLVL_DEBUG + 33) << __func__ << ":" << __LINE__ << " searchString=" << searchString << ", targetLocation=" << targetLocation;
    1406            0 :                         while (targetLocation != std::string::npos)
    1407              :                         {
    1408            0 :                                 modifiedPattern.replace(targetLocation, searchString.length(), newString);
    1409            0 :                                 targetLocation = modifiedPattern.find(searchString);
    1410            0 :                                 TLOG(TLVL_DEBUG + 33) << __func__ << ":" << __LINE__ << " searchString=" << searchString << ", targetLocation=" << targetLocation;
    1411              :                         }
    1412              :                 }
    1413            0 :         }
    1414              : 
    1415            0 :         TLOG(TLVL_DEBUG + 32) << __func__ << ": modifiedPattern = \"" << modifiedPattern << "\"";
    1416            0 :         return modifiedPattern;
    1417            0 : }
    1418              : 
    1419              : }  // namespace art
    1420              : 
    1421            0 : DEFINE_ART_MODULE(art::RootDAQOutMF)  // NOLINT(performance-unnecessary-value-param)
        

Generated by: LCOV version 2.0-1