diff --git a/CMakeLists.txt b/CMakeLists.txt index 72410085a9ee8570c387cadeb48c4dd07a9af15c..39428fc7170b0c504faee6cf53a2bd39c3afe211 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -261,6 +261,7 @@ add_subdirectory (algo) add_subdirectory (sim) add_subdirectory (reco) add_subdirectory (analysis) +add_subdirectory (services) ### Others Option(LARGE_TEST_STATISTIC "Run the test suite with large statistic (100 events)" OFF) diff --git a/services/CMakeLists.txt b/services/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..e34c0786ba15c6f4787fff5ef585ae4efe471c1b --- /dev/null +++ b/services/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(histserv) diff --git a/services/histserv/CMakeLists.txt b/services/histserv/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..9bd62df1441dccb20c1caf2f6074f088a04208a0 --- /dev/null +++ b/services/histserv/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(app) +add_subdirectory(lib) diff --git a/services/histserv/app/Application.cxx b/services/histserv/app/Application.cxx new file mode 100644 index 0000000000000000000000000000000000000000..d5ca5d33d95f3b3f3ba0d51c9308c6ee413e12fa --- /dev/null +++ b/services/histserv/app/Application.cxx @@ -0,0 +1,585 @@ +/* Copyright (C) 2023 Facility for Antiproton and Ion Research in Europe, Darmstadt + SPDX-License-Identifier: GPL-3.0-only + Authors: Pierre-Alain Loizeau [committer] */ + +#include "Application.h" + +#include "CbmFlesCanvasTools.h" + +#include <Logger.h> + +#include "TCanvas.h" +#include "TEnv.h" +#include "TFile.h" +#include "TH1.h" +#include "TH2.h" +#include "THttpServer.h" +#include "TMessage.h" +#include "TObjArray.h" +#include "TProfile.h" +#include "TRootSniffer.h" +#include "TSystem.h" + +#include <boost/serialization/utility.hpp> + +#include <mutex> + +#include "Histo1D.h" +#include "ui_callbacks.h" + +std::mutex mtx; + +namespace cbm::services::histserv +{ + + // ----- Constructor --------------------------------------------------------------------------------------------- + Application::Application(ProgramOptions const& opt) : fOpt(opt) + { + /// Read options from executable + LOG(info) << "Options for Application."; + // FIXME: alternative to FairMQ fsChannelNameHistosInput = fConfig->GetValue<std::string>("ChNameIn"); + LOG(info) << " HTTP server port: " << fOpt.HttpPort(); + if ("" != fOpt.HistoFile()) { // + LOG(info) << "Output filename: " << fOpt.HistoFile() << (fOpt.Overwrite() ? " (in overwrite mode)" : ""); + } + + /// FIXME: SOMETHING_To_Replace_FairMQ!!!!!!!!!!!!! + /// FIXME: Initialize communication channels of SOMETHING_To_Replace_FairMQ + /// FIXME: Link channel to method in order to process received messages + /* + /// Method processing either combined Config+Data or Data only + OnData(fsChannelNameHistosInput, &CbmMqHistoServer::ReceiveConfigAndData); + * */ + + fServer = new THttpServer(Form("http:%u", fOpt.HttpPort())); + /// To avoid the server sucking all Histos from gROOT when no output file is used + fServer->GetSniffer()->SetScanGlobalDir(kFALSE); + const char* jsrootsys = gSystem->Getenv("JSROOTSYS"); + if (!jsrootsys) jsrootsys = gEnv->GetValue("HttpServ.JSRootPath", jsrootsys); + + fServer->RegisterCommand("/Reset_Hist", "bHistoServerResetHistos=true"); + fServer->RegisterCommand("/Save_Hist", "bHistoServerSaveHistos=true"); + fServer->RegisterCommand("/Stop_Server", "bHistoServerStop=true"); + + /* + fServer->RegisterCommand("/Reset_Hist", "this->ResetHistograms()"); + fServer->RegisterCommand("/Save_Hist", "this->SaveHistograms()"); + */ + + fServer->Restrict("/Reset_Hist", "allow=admin"); + fServer->Restrict("/Save_Hist", "allow=admin"); + fServer->Restrict("/Stop_Server", "allow=admin"); + + + LOG(info) << "JSROOT location: " << jsrootsys; + } + // ------------------------------------------------------------------------------------------------------------------- + + + // ----- Main Loop ----------------------------------------------------------------------------------------------- + void Application::Exec() + { + fStopThread = false; + fThread = std::thread(&Application::UpdateHttpServer, this); + + while (!bHistoServerStop) { // + /// Infinite loop, this is a service which should survive until told otherwise after all + + /// FIXME: Start listening to <SOMETHING?!?> to receive histograms and configuration + + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + } + // ------------------------------------------------------------------------------------------------------------------- + + // ----- Constructor --------------------------------------------------------------------------------------------- + Application::~Application() + { + SaveHistograms(); + fStopThread = true; + fThread.join(); + SaveHistograms(); + } + // ------------------------------------------------------------------------------------------------------------------- + + // ----- Server update background thread ------------------------------------------------------------------------- + void Application::UpdateHttpServer() + { + /// This is needed to have a reactive GUI independently of histogram updates reception + while (!fStopThread) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + std::lock_guard<std::mutex> lk(mtx); + + fServer->ProcessRequests(); + + /// TODO: control flags communication from histo server to histograms sources? + /// Idea: 1 req channel (per process or not, mixup?), polling every N TS and/or M s + if (bHistoServerResetHistos) { + LOG(info) << "Reset Monitor histos "; + ResetHistograms(); + bHistoServerResetHistos = false; + } // if( bHistoServerResetHistos ) + + if (bHistoServerSaveHistos) { + LOG(info) << "Save All histos & canvases"; + SaveHistograms(); + bHistoServerSaveHistos = false; + } // if( bHistoServerSaveHistos ) + } + } + // ------------------------------------------------------------------------------------------------------------------- + + // ----- Server update background thread ------------------------------------------------------------------------- + bool Application::ReceiveData(/*FIXME_SOMETHING_To_Replace_FairMQ& parts*/) + { + LOG(debug) << "Application::ReceiveData => Processing histograms update"; + TObject* tempObject = nullptr; + + /// FIXME: Something to replace FairMQ and extract the histograms!!!! + /// FIXME: Need something to replace the ROOT serializer which allowed to have any of TH1x, TH2x, TH3x or TProfile + cbm::algo::Histo1D source(1, 0.0, 1.0, "dummy"); + + /// copied from CbmTaskDigiEventQa::ToTH1D + TH1D* result = + new TH1D(source.Name().c_str(), source.Name().c_str(), source.NumBins(), source.MinValue(), source.MaxValue()); + for (uint32_t bin = 0; bin < source.NumBins(); bin++) { + result->SetBinContent(bin, source.Content(bin)); + } + result->SetEntries(source.NumEntries()); + tempObject = result; + + if (TString(tempObject->ClassName()).EqualTo("TObjArray")) { + std::lock_guard<std::mutex> lk(mtx); + TObjArray* arrayHisto = static_cast<TObjArray*>(tempObject); + for (Int_t i = 0; i < arrayHisto->GetEntriesFast(); i++) { + TObject* pObj = arrayHisto->At(i); + + if (nullptr != dynamic_cast<TProfile*>(pObj)) { + if (!ReadHistogram<TProfile>(dynamic_cast<TProfile*>(pObj))) { // + return false; + } + } // if( nullptr != dynamic_cast< TProfile *>( pObj ) ) + else if (nullptr != dynamic_cast<TH2*>(pObj)) { + if (!ReadHistogram<TH2>(dynamic_cast<TH2*>(pObj))) { // + return false; + } + } // if( nullptr != dynamic_cast< TH2 *>( pObj ) ) + else if (nullptr != dynamic_cast<TH1*>(pObj)) { + if (!ReadHistogram<TH1>(dynamic_cast<TH1*>(pObj))) { // + return false; + } + } // if( nullptr != dynamic_cast< TH1 *>( pObj ) ) + else + LOG(warning) << "Unsupported object type for " << pObj->GetName(); + } // for (Int_t i = 0; i < arrayHisto->GetEntriesFast(); i++) + + LOG(debug) << "Application::ReceiveData => Deleting array"; + /// Need to use Delete instead of Clear to avoid memory leak!!! + arrayHisto->Delete(); + + /// If new histos received, try to prepare as many canvases as possible + /// Should be expensive on start and cheap afterward + if (!fbAllCanvasReady) { + LOG(debug) << "Application::ReceiveData => Checking for canvases updates"; + for (uint32_t uCanv = 0; uCanv < fvpsCanvasConfig.size(); ++uCanv) { + /// Jump canvases already ready + if (fvbCanvasReady[uCanv]) { // + continue; + } + + /// Now come the expensive part as we unpack its config and check each histo + fvbCanvasReady[uCanv] = PrepareCanvas(uCanv); + } // for( uint32_t uCanv = 0; uCanv < fvpsCanvasConfig.size(); ++uCanv ) + } // if( !fbAllCanvasReady ) + } // if (TString(tempObject->ClassName()).EqualTo("TObjArray")) + else { + fStopThread = true; + std::string err_msg = "Application::ReceiveData => Wrong object type at input: "; + err_msg += tempObject->ClassName(); + throw std::runtime_error(err_msg); + } + + fNMessages += 1; + + if (nullptr != tempObject) delete tempObject; + + LOG(debug) << "Application::ReceiveData => Finished processing histograms update"; + + return true; + } + + bool Application::ReceiveHistoConfig(/*FIXME_SOMETHING_To_Replace_FairMQ& parts*/) + { + std::pair<std::string, std::string> tempObject("", ""); + + /// FIXME: Something to replace FairMQ and extract the histograms!!!! + // Deserialize<BoostSerializer<std::pair<std::string, std::string>>>(*msg, tempObject); + // BoostSerializer<std::pair<std::string, std::string>>().Deserialize(*msg, tempObject); + + + LOG(info) << " Received configuration for histo " << tempObject.first << " : " << tempObject.second; + + /// Check if histo name already received in previous messages + /// Linear search should be ok as config is shared only at startup + UInt_t uPrevHist = 0; + for (uPrevHist = 0; uPrevHist < fvpsHistosFolder.size(); ++uPrevHist) { + if (fvpsHistosFolder[uPrevHist].first == tempObject.first) { // + break; + } + } // for( UInt_t uPrevHist = 0; uPrevHist < fvpsHistosFolder.size(); ++uPrevHist ) + + if (uPrevHist < fvpsHistosFolder.size()) { + LOG(info) << " Ignored new configuration for histo " << tempObject.first + << " due to previously received one: " << tempObject.second; + /// Not sure if we should return false here... + } // if( uPrevHist < fvpsHistosFolder.size() ) + else { + fvpsHistosFolder.push_back(tempObject); + fvHistos.push_back(std::pair<TNamed*, std::string>(nullptr, "")); + fvbHistoRegistered.push_back(false); + fbAllHistosRegistered = false; + } // else of if( uPrevHist < fvpsHistosFolder.size() ) + + return true; + } + + bool Application::ReceiveCanvasConfig(/*FIXME_SOMETHING_To_Replace_FairMQ& parts*/) + { + std::pair<std::string, std::string> tempObject("", ""); + + /// FIXME: Something to replace FairMQ and extract the histograms!!!! + // Deserialize<BoostSerializer<std::pair<std::string, std::string>>>(*msg, tempObject); + // BoostSerializer<std::pair<std::string, std::string>>().Deserialize(*msg, tempObject); + + LOG(info) << " Received configuration for canvas " << tempObject.first << " : " << tempObject.second; + + /// Check if canvas name already received in previous messages + /// Linear search should be ok as config is shared only at startup + uint32_t uPrevCanv = 0; + for (uPrevCanv = 0; uPrevCanv < fvpsCanvasConfig.size(); ++uPrevCanv) { + if (fvpsCanvasConfig[uPrevCanv].first == tempObject.first) { // + break; + } + } // for( UInt_t uPrevCanv = 0; uPrevCanv < fvpsCanvasConfig.size(); ++uPrevCanv ) + + if (uPrevCanv < fvpsCanvasConfig.size()) { + LOG(warning) << " Ignored new configuration for Canvas " << tempObject.first + << " due to previously received one: " << tempObject.second; + /// Not sure if we should return false here... + } // if( uPrevCanv < fvpsCanvasConfig.size() ) + else { + fvpsCanvasConfig.push_back(tempObject); + fvbCanvasReady.push_back(false); + fbAllCanvasReady = false; + + fvCanvas.push_back(std::pair<TCanvas*, std::string>(nullptr, "")); + fvbCanvasRegistered.push_back(false); + fbAllCanvasRegistered = false; + } // else of if( uPrevCanv < fvpsCanvasConfig.size() ) + return true; + } + + bool Application::ReceiveConfigAndData(/*FIXME_SOMETHING_To_Replace_FairMQ& parts*/) + { + /// FIXME: Something to replace FairMQ and extract the histograms!!!! + /* + /// Reject anything but + /// - Header + Histo Config + Canvas Config + Histo Data + /// - Histo data only + if (4 != parts.Size()) { + if (1 == parts.Size()) { + /// Catch case of having only the histograms updates + return ReceiveData(parts.At(0), 0); + } // if( 1 == parts.Size() ) + + fStopThread = true; + std::string err_msg = "Application::ReceiveConfigAndData => Wrong number of parts: "; + err_msg += parts.Size(); + err_msg += " instead of 4 (Header + Histo Config + Canvas config + Data) or 1 (Data)!"; + throw std::runtime_error(err_msg); + } // if( parts.Size() < 4 ) + + LOG(info) << "Application::ReceiveConfigAndData => Received composed message with " << parts.Size() << " parts"; + + /// Header contains a pair of + std::pair<uint32_t, uint32_t> pairHeader; + // Deserialize<BoostSerializer<std::pair<uint32_t, uint32_t>>>(*parts.At(0), pairHeader); + BoostSerializer<std::pair<uint32_t, uint32_t>>().Deserialize(*parts.At(0), pairHeader); + + LOG(info) << "Application::ReceiveConfigAndData => Received configuration for " << pairHeader.first + << " histos and " << pairHeader.second << " canvases"; + + uint32_t uOffsetHistoConfig = pairHeader.first; + if (0 == pairHeader.first) { + uOffsetHistoConfig = 1; + if (0 < (parts.At(uOffsetHistoConfig))->GetSize()) { + fStopThread = true; + std::string err_msg = "Application::ReceiveConfigAndData => No histo config expected but corresponding message"; + err_msg += " is not empty: "; + err_msg += (parts.At(uOffsetHistoConfig))->GetSize(); + throw std::runtime_error(err_msg); + } + } + + uint32_t uOffsetCanvasConfig = pairHeader.second; + if (0 == pairHeader.second) { + uOffsetCanvasConfig = 1; + if (0 < (parts.At(uOffsetHistoConfig + uOffsetCanvasConfig))->GetSize()) { + fStopThread = true; + std::string err_msg = "Application::ReceiveConfigAndData => No Canvas config expected but corresponding message"; + err_msg += " is not empty: "; + err_msg += (parts.At(uOffsetHistoConfig + uOffsetCanvasConfig))->GetSize(); + throw std::runtime_error(err_msg); + } + } + + if (static_cast<size_t>(parts.Size()) != 1 + uOffsetHistoConfig + uOffsetCanvasConfig + 1) { + fStopThread = true; + std::string err_msg = "Application::ReceiveConfigAndData => Number of parts not matching header: "; + err_msg += parts.Size(); + err_msg += " instead of "; + err_msg += 1 + uOffsetHistoConfig + uOffsetCanvasConfig + 1; + throw std::runtime_error(err_msg); + } // if( parts.Size() != 1 + pairHeader.first + pairHeader.second ) + + /// Decode parts for histograms configuration + for (uint32_t uHisto = 0; uHisto < pairHeader.first; ++uHisto) { + ReceiveHistoConfig(parts.At(1 + uHisto), 0); + } // for (UInt_t uHisto = 0; uHisto < pairHeader.first; ++uHisto) + + /// Decode parts for histograms configuration + for (uint32_t uCanv = 0; uCanv < pairHeader.second; ++uCanv) { + ReceiveCanvasConfig(parts.At(1 + uOffsetHistoConfig + uCanv), 0); + } // for (UInt_t uCanv = 0; uCanv < pairHeader.second; ++uCanv) + + /// Decode the histograms data now that the configuration is loaded + ReceiveData(parts.At(1 + uOffsetHistoConfig + uOffsetCanvasConfig), 0); + * + LOG(info) << "Application::ReceiveConfigAndData => Finished processing composed message with " << parts.Size() + << " parts"; + */ + + return true; + } + // ------------------------------------------------------------------------------------------------------------------- + + template<class HistoT> + bool Application::ReadHistogram(HistoT* pHist) + { + int index1 = FindHistogram(pHist->GetName()); + if (-1 == index1) { + HistoT* histogram_new = static_cast<HistoT*>(pHist->Clone()); + fArrayHisto.Add(histogram_new); + + LOG(info) << "Received new histo " << pHist->GetName(); + + /// If new histo received, try to register it if configuration available + if (!fbAllHistosRegistered) { + for (uint32_t uHist = 0; uHist < fvpsHistosFolder.size(); ++uHist) { + /// Jump histos already ready + if (fvbHistoRegistered[uHist]) { // + continue; + } + + /// Check if name matches one in config for others + if (fvpsHistosFolder[uHist].first == histogram_new->GetName()) { + fvHistos[uHist] = std::pair<TNamed*, std::string>(histogram_new, fvpsHistosFolder[uHist].second); + fServer->Register(Form("/%s", fvHistos[uHist].second.data()), fvHistos[uHist].first); + fvbHistoRegistered[uHist] = true; + + LOG(info) << "registered histo " << fvHistos[uHist].first->GetName() << " in folder " + << fvHistos[uHist].second; + + + /// Update flag telling whether all known histos are registered + fbAllHistosRegistered = true; + for (uint32_t uIdx = 0; uIdx < fvbHistoRegistered.size(); ++uIdx) { + if (!fvbHistoRegistered[uIdx]) { + fbAllHistosRegistered = false; + break; + } // if( !fvbHistoRegistered[ uIdx ] ) + } // for( uint32_t uIdx = 0; uIdx < fvbHistoRegistered.size(); ++uIdx ) + + break; + } // if( fvpsHistosFolder[ uHist ].first == histogram_new->GetName() ) + } // for( uint32_t uCanv = 0; uCanv < fvpsCanvasConfig.size(); ++uCanv ) + } // if( !fbAllCanvasReady ) + } // if (-1 == index1) + else { + LOG(debug) << "Received update for: " << pHist->GetName(); + HistoT* histogram_existing = dynamic_cast<HistoT*>(fArrayHisto.At(index1)); + if (nullptr == histogram_existing) { + LOG(error) << "CbmMqHistoServer::ReadHistogram => " + << "Incompatible type found during update for histo " << pHist->GetName(); + return false; + } // if( nullptr == histogram_existing ) + + histogram_existing->Add(pHist); + } // else of if (-1 == index1) + return true; + } + + int Application::FindHistogram(const std::string& name) + { + for (int iHist = 0; iHist < fArrayHisto.GetEntriesFast(); ++iHist) { + TObject* obj = fArrayHisto.At(iHist); + if (TString(obj->GetName()).EqualTo(name)) { // + return iHist; + } + } // for( int iHist = 0; iHist < fArrayHisto.GetEntriesFast(); ++iHist ) + return -1; + } + + bool Application::PrepareCanvas(uint32_t uCanvIdx) + { + LOG(debug) << " Extracting configuration for canvas index " << uCanvIdx; + CanvasConfig conf(ExtractCanvasConfigFromString(fvpsCanvasConfig[uCanvIdx].second)); + + /// First check if all objects to be drawn are present + uint32_t uNbPads(conf.GetNbPads()); + for (uint32_t uPadIdx = 0; uPadIdx < uNbPads; ++uPadIdx) { + uint32_t uNbObj(conf.GetNbObjsInPad(uPadIdx)); + for (uint32_t uObjIdx = 0; uObjIdx < uNbObj; ++uObjIdx) { + std::string sName(conf.GetObjName(uPadIdx, uObjIdx)); + /// Check for empty pads! + if ("nullptr" != sName) { + if (FindHistogram(sName) < 0) { + return false; + } // if( FindHistogram( conf.GetObjName( uPadIdx, uObjIdx ) ) < 0 ) + } // if( "nullptr" != sName ) + } // for( uint32_t uObjIdx = 0; uObjIdx < uNbObj; ++uObjIdx ) + } // for( uint32_t uPadIdx = 0; uPadIdx < uNbPads; ++uPadIdx ) + + LOG(info) << " All histos found for canvas " << conf.GetName().data() << ", now preparing it"; + + /// Create new canvas and pads + TCanvas* pNewCanv = new TCanvas(conf.GetName().data(), conf.GetTitle().data()); + pNewCanv->Divide(conf.GetNbPadsX(), conf.GetNbPadsY()); + + /// Loop on pads + for (uint32_t uPadIdx = 0; uPadIdx < uNbPads; ++uPadIdx) { + pNewCanv->cd(1 + uPadIdx); + + /// Pad settings + gPad->SetGrid(conf.GetGridx(uPadIdx), conf.GetGridy(uPadIdx)); + gPad->SetLogx(conf.GetLogx(uPadIdx)); + gPad->SetLogy(conf.GetLogy(uPadIdx)); + gPad->SetLogz(conf.GetLogz(uPadIdx)); + + /// Add objects (we know they are there + uint32_t uNbObj(conf.GetNbObjsInPad(uPadIdx)); + for (uint32_t uObjIdx = 0; uObjIdx < uNbObj; ++uObjIdx) { + std::string sName(conf.GetObjName(uPadIdx, uObjIdx)); + if ("nullptr" != sName) { + TObject* pObj = fArrayHisto[FindHistogram(sName)]; + + if (nullptr != dynamic_cast<TProfile*>(pObj)) { + dynamic_cast<TProfile*>(pObj)->Draw(conf.GetOption(uPadIdx, uObjIdx).data()); + } // if( nullptr != dynamic_cast< TProfile *>( pObj ) ) + else if (nullptr != dynamic_cast<TH2*>(pObj)) { + dynamic_cast<TH2*>(pObj)->Draw(conf.GetOption(uPadIdx, uObjIdx).data()); + } // if( nullptr != dynamic_cast< TH2 *>( pObj ) ) + else if (nullptr != dynamic_cast<TH1*>(pObj)) { + dynamic_cast<TH1*>(pObj)->Draw(conf.GetOption(uPadIdx, uObjIdx).data()); + } // if( nullptr != dynamic_cast< TH1 *>( pObj ) ) + else + LOG(warning) << " Unsupported object type for " << sName << " when preparing canvas " << conf.GetName(); + + LOG(info) << " Configured histo " << sName << " on pad " << (1 + uPadIdx) << " for canvas " + << conf.GetName().data(); + } // if( "nullptr" != sName ) + } // for( uint32_t uObjIdx = 0; uObjIdx < uNbObj; ++uObjIdx ) + } // for( uint32_t uPadIdx = 0; uPadIdx < uNbPads; ++uPadIdx ) + + fvCanvas[uCanvIdx] = std::pair<TCanvas*, std::string>(pNewCanv, "canvases"); + fServer->Register(Form("/%s", fvCanvas[uCanvIdx].second.data()), fvCanvas[uCanvIdx].first); + fvbCanvasRegistered[uCanvIdx] = true; + + LOG(info) << " Registered canvas " << fvCanvas[uCanvIdx].first->GetName() << " in folder " + << fvCanvas[uCanvIdx].second; + + /// Update flag telling whether all known canvases are registered + fbAllCanvasRegistered = true; + for (uint32_t uIdx = 0; uIdx < fvbCanvasRegistered.size(); ++uIdx) { + if (!fvbCanvasRegistered[uIdx]) { + fbAllCanvasRegistered = false; + break; + } // if( !fvbCanvasRegistered[ uIdx ] ) + } // for( uint32_t uIdx = 0; uIdx < fvbCanvasRegistered.size(); ++uIdx ) + + return true; + } + + bool Application::ResetHistograms() + { + for (int iHist = 0; iHist < fArrayHisto.GetEntriesFast(); ++iHist) { + dynamic_cast<TH1*>(fArrayHisto.At(iHist))->Reset(); + } // for( int iHist = 0; iHist < fArrayHisto.GetEntriesFast(); ++iHist ) + return true; + } + + bool Application::SaveHistograms() + { + if ("" == fOpt.HistoFile()) { // + LOG(error) << "Filename for saving histograms and canvases not defined. Ignoring request."; + return false; + } + + /// Save old global file and folder pointer to avoid messing with FairRoot + TFile* oldFile = gFile; + TDirectory* oldDir = gDirectory; + + /// (Re-)Create ROOT file to store the histos + TFile* histoFile = nullptr; + + // open separate histo file in recreate mode + histoFile = new TFile(fOpt.HistoFile().data(), fOpt.Overwrite() ? "RECREATE" : "CREATE"); + + if (nullptr == histoFile) { // + gFile = oldFile; + gDirectory = oldDir; + LOG(error) << "Ignoring request to save histograms and canvases: could not open output file " << fOpt.HistoFile(); + return false; + } + + LOG(info) << "Saving Histograms and canvases in file: " << fOpt.HistoFile(); + + /// Register the histos in the HTTP server + for (UInt_t uHisto = 0; uHisto < fvHistos.size(); ++uHisto) { + /// Make sure we end up in chosen folder + TString sFolder = fvHistos[uHisto].second.data(); + if (nullptr == gDirectory->Get(sFolder)) { // + gDirectory->mkdir(sFolder); + } + gDirectory->cd(sFolder); + + /// Write plot + fvHistos[uHisto].first->Write(); + + histoFile->cd(); + } // for( UInt_t uHisto = 0; uHisto < fvHistos.size(); ++uHisto ) + + for (UInt_t uCanvas = 0; uCanvas < fvCanvas.size(); ++uCanvas) { + /// Make sure we end up in chosen folder + TString sFolder = fvCanvas[uCanvas].second.data(); + if (nullptr == gDirectory->Get(sFolder)) { // + gDirectory->mkdir(sFolder); + } + gDirectory->cd(sFolder); + + /// Write plot + fvCanvas[uCanvas].first->Write(); + + histoFile->cd(); + } // for( UInt_t uHisto = 0; uHisto < fvCanvas.size(); ++uHisto ) + + /// Restore old global file and folder pointer to avoid messing with FairRoot + gFile = oldFile; + gDirectory = oldDir; + + histoFile->Close(); + + return true; + } + +} // namespace cbm::services::histserv diff --git a/services/histserv/app/Application.h b/services/histserv/app/Application.h new file mode 100644 index 0000000000000000000000000000000000000000..2b8168df58fdbc9c9dba17d0f659ad759d2e1bd3 --- /dev/null +++ b/services/histserv/app/Application.h @@ -0,0 +1,94 @@ +/* Copyright (C) 2023 Facility for Antiproton and Ion Research in Europe, Darmstadt + SPDX-License-Identifier: GPL-3.0-only + Authors: Pierre-Alain Loizeau [committer] */ + +#ifndef CBM_SERVICES_HISTSERV_APP_APPLICATION_H +#define CBM_SERVICES_HISTSERV_APP_APPLICATION_H 1 + +#include "THttpServer.h" +#include "TObjArray.h" + +#include <memory> +#include <string> +#include <thread> + +#include "ProgramOptions.h" + +class TNamed; +class TCanvas; + +namespace cbm::services::histserv +{ + + class Application { + + + public: + /** @brief Standard constructor, initialises the application + ** @param opt **/ + explicit Application(ProgramOptions const& opt); + + /** @brief Copy constructor forbidden **/ + Application(const Application&) = delete; + + /** @brief Assignment operator forbidden **/ + void operator=(const Application&) = delete; + + /** @brief Destructor **/ + ~Application(); + + /** @brief Run the application **/ + void Exec(); + + void UpdateHttpServer(); + + private: + const std::string& OutputFile() const; + const std::string& ParamFile() const; + const std::string& SetupTag() const; + //const std::string& ConfigFile() const; + + bool ReceiveData(/*FIXME_SOMETHING_To_Replace_FairMQ& parts*/); + bool ReceiveHistoConfig(/*FIXME_SOMETHING_To_Replace_FairMQ& parts*/); + bool ReceiveCanvasConfig(/*FIXME_SOMETHING_To_Replace_FairMQ& parts*/); + bool ReceiveConfigAndData(/*FIXME_SOMETHING_To_Replace_FairMQ& parts*/); + + template<class HistoT> + bool ReadHistogram(HistoT* pHist); + int FindHistogram(const std::string& name); + bool PrepareCanvas(uint32_t uCanvIdx); + + bool ResetHistograms(); + bool SaveHistograms(); + + private: + ProgramOptions const& fOpt; ///< Program options object + THttpServer* fServer = nullptr; ///< ROOT Histogram server (JSroot) + std::thread fThread; + bool fStopThread = false; + + /// Array of histograms with unique names + TObjArray fArrayHisto; + /// Vector of string with ( HistoName, FolderPath ) to configure the histogram + std::vector<std::pair<std::string, std::string>> fvpsHistosFolder = {}; + /// Vector of string pairs with ( CanvasName, CanvasConfig ) to configure the canvases and histos within + /// Format of Can config is "Name;Title;NbPadX(U);NbPadY(U);ConfigPad1(s);....;ConfigPadXY(s)" + /// Format of Pad config is "GrixX(b),GridY(b),LogX(b),LogY(b),LogZ(b),HistoName(s),DrawOptions(s)" + std::vector<std::pair<std::string, std::string>> fvpsCanvasConfig = {}; + std::vector<bool> fvbCanvasReady = {}; + bool fbAllCanvasReady = false; + + std::vector<std::pair<TNamed*, std::string>> fvHistos = {}; ///< Vector of Histos pointers and folder path + std::vector<bool> fvbHistoRegistered = {}; + bool fbAllHistosRegistered = false; + std::vector<std::pair<TCanvas*, std::string>> fvCanvas = {}; ///< Vector of Canvas pointers and folder path + std::vector<bool> fvbCanvasRegistered = {}; + bool fbAllCanvasRegistered = false; + + /// Internal status + uint32_t fNMessages = 0; + }; + +} // namespace cbm::services::histserv + +#endif /* CBM_SERVICES_HISTSERV_APP_APPLICATION_H */ diff --git a/services/histserv/app/CMakeLists.txt b/services/histserv/app/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..b614872a252d4a900e0b7d2ca64a2a34822da4c4 --- /dev/null +++ b/services/histserv/app/CMakeLists.txt @@ -0,0 +1,43 @@ +# CMakeList file for binary histserv_nofairmq +# P.-A. Loizeau, 26 June 2023 + +set(INCLUDE_DIRECTORIES + ${INCLUDE_DIRECTORIES} + ${CMAKE_CURRENT_SOURCE_DIR} + ${CBMROOT_SOURCE_DIR}/algo/qa/ + ) + +set(SRCS + Application.cxx + ProgramOptions.cxx + main.cxx + ) + +set(HEADERS + Application.h + ProgramOptions.h + ) + +set(INCLUDE_DIRECTORIES + ${INCLUDE_DIRECTORIES} + ${CBMROOT_SOURCE_DIR} + ) + +add_executable(histserv_nofairmq ${SRCS} ${HEADERS}) + +target_link_libraries(histserv_nofairmq + PUBLIC + Algo + CbmFlibFlesTools + CbmServicesHistServ + PRIVATE + Boost::program_options + FairLogger::FairLogger + ROOT::Core + ROOT::Gpad + ROOT::Hist + ROOT::RIO + ROOT::RHTTP + ) + +install(TARGETS histserv_nofairmq DESTINATION bin) diff --git a/services/histserv/app/ProgramOptions.cxx b/services/histserv/app/ProgramOptions.cxx new file mode 100644 index 0000000000000000000000000000000000000000..d5826bbe284c49fb3331d05d9b862264729a134c --- /dev/null +++ b/services/histserv/app/ProgramOptions.cxx @@ -0,0 +1,69 @@ +/* Copyright (C) 2023 Facility for Antiproton and Ion Research in Europe, Darmstadt + SPDX-License-Identifier: GPL-3.0-only + Authors: Pierre-Alain Loizeau [committer] */ + +#include "ProgramOptions.h" + +#include <Logger.h> + +#include <boost/program_options.hpp> + +#include <iostream> + +namespace po = boost::program_options; + +using std::string; + +namespace cbm::services::histserv +{ + + // ----- Parse command line --------------------------------------------- + void ProgramOptions::ParseOptions(int argc, char* argv[]) + { + + // --- Define generic options + po::options_description generic("Generic options"); + auto generic_add = generic.add_options(); + generic_add("help,h", "display this help and exit"); + + // --- Build default config file path + //string defconfig = std::getenv("VMCWORKDIR"); + //defconfig.append("/"); + //defconfig.append(DEFAULT_CONFIG); + + // --- Define configuration options + po::options_description config("Configuration"); + auto config_add = config.add_options(); + config_add("input,i", po::value<string>(&fsChanHistosIn)->value_name("<???????>"), + "name or host:port or whatever is needed for input channel (histos/canvases config and data)"); + config_add("port,p", po::value<uint32_t>(&fuHttpServerPort)->default_value(8080), + "port on which the http ROOT server (JSroot) will be available"); + config_add("output,o", po::value<string>(&fsHistoFileName)->value_name("<file name>"), + "name of the output ROOT file with histograms backup"); + // config_add("config,c", po::value<string>(&fConfig)->value_name("<file name>")->default_value(defconfig), + // "name of a YAML file describing the configuration of reconstruction"); + config_add("overwrite,w", po::bool_switch(&fOverwrite)->default_value(false), + "allow to overwite an existing output file"); + + // --- Allowed options + po::options_description cmdline_options("Allowed options"); + cmdline_options.add(generic).add(config); + + // --- Parse command line + po::variables_map vars; + po::store(po::parse_command_line(argc, argv, cmdline_options), vars); + po::notify(vars); + + // --- Help: print help information and exit program + if (vars.count("help") != 0u) { + std::cout << cmdline_options << std::endl; + exit(EXIT_SUCCESS); + } + + // --- Catch mandatory parameters not being specified + if (vars.count("input") == 0u) { throw std::runtime_error("no input channel specified"); } + if (vars.count("port") == 0u) { throw std::runtime_error("no http port specified"); } + } + // -------------------------------------------------------------------------- + +} // namespace cbm::services::histserv diff --git a/services/histserv/app/ProgramOptions.h b/services/histserv/app/ProgramOptions.h new file mode 100644 index 0000000000000000000000000000000000000000..6e7a526bc265ac074b1fd4832382f4c705ac7d2a --- /dev/null +++ b/services/histserv/app/ProgramOptions.h @@ -0,0 +1,63 @@ +/* Copyright (C) 2023 Facility for Antiproton and Ion Research in Europe, Darmstadt + SPDX-License-Identifier: GPL-3.0-only + Authors: Pierre-Alain Loizeau [committer] */ + +#ifndef CBM_SERVICES_HISTSERV_APP_PROGRAMOPTIONS_H +#define CBM_SERVICES_HISTSERV_APP_PROGRAMOPTIONS_H 1 + +#include <string> + +namespace cbm::services::histserv +{ + + /** @class ProgramOptions + ** @author Pierre-Alain Loizeau <p.-a.loizeau@gsi.de> + ** @since 26 June 2023 + ** + ** Program option class for the application histserv_nofairmq + **/ + class ProgramOptions { + public: + /** @brief Standard constructor with command line arguments **/ + ProgramOptions(int argc, char* argv[]) { ParseOptions(argc, argv); } + + /** @brief Copy constructor forbidden **/ + ProgramOptions(const ProgramOptions&) = default; + + /** @brief Assignment operator forbidden **/ + ProgramOptions& operator=(const ProgramOptions&) = default; + + /** @brief Destructor **/ + ~ProgramOptions() = default; + + /** @brief Get interface channel name or hostname + port or whatever or ????? (FIXME: replacement of FairMQ) **/ + [[nodiscard]] const std::string& ComChan() const { return fsChanHistosIn; } + + /** @brief Get histo server http port **/ + [[nodiscard]] const uint32_t& HttpPort() const { return fuHttpServerPort; } + + /** @brief Get output file name (.root format) **/ + [[nodiscard]] const std::string& HistoFile() const { return fsHistoFileName; } + + /** @brief Get overwrite option **/ + [[nodiscard]] bool Overwrite() const { return fOverwrite; } + + // /** @brief Get configuration file name (YAML format) **/ + // [[nodiscard]] const std::string& ConfigFile() const { return fConfig; } + + private: + /** @brief Parse command line arguments using boost program_options **/ + void ParseOptions(int argc, char* argv[]); + + + private: // members + std::string fsChanHistosIn = "histogram-in"; + uint32_t fuHttpServerPort = 8080; ///< HTTP port of the ROOT web server + std::string fsHistoFileName = "histos_dump.root"; ///< Output file name (ROOT format) + bool fOverwrite = false; ///< Enable overwriting of existing output file + //std::string fConfig = ""; ///< Configuration file name (YAML format) + }; + +} // namespace cbm::services::histserv + +#endif /* CBM_SERVICES_HISTSERV_APP_PROGRAMOPTIONS_H */ diff --git a/services/histserv/app/main.cxx b/services/histserv/app/main.cxx new file mode 100644 index 0000000000000000000000000000000000000000..4dbaa8862b133acb0bcdf344088ea1fe386f0b77 --- /dev/null +++ b/services/histserv/app/main.cxx @@ -0,0 +1,26 @@ +/* Copyright (C) 2023 Facility for Antiproton and Ion Research in Europe, Darmstadt + SPDX-License-Identifier: GPL-3.0-only + Authors: Pierre-Alain Loizeau [committer] */ +#include <Logger.h> + +#include "Application.h" +#include "ProgramOptions.h" + +using namespace cbm::services::histserv; + +int main(int argc, char* argv[]) +{ + LOG(info) << "***** Histogram server without FairMQ *****"; + try { + ProgramOptions opt(argc, argv); + Application app(opt); + app.Exec(); + } + catch (std::exception const& e) { + LOG(error) << e.what() << "; terminating."; + return EXIT_FAILURE; + } + + LOG(info) << "Histogram server without FairMQ: Program completed successfully; exiting."; + return EXIT_SUCCESS; +} diff --git a/services/histserv/lib/CMakeLists.txt b/services/histserv/lib/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..790bd92aa1f56383a3e20681833cf981c3117b5e --- /dev/null +++ b/services/histserv/lib/CMakeLists.txt @@ -0,0 +1,21 @@ +# CMakeList file for library associated to HistServ service, needed by CLING to allow commands in WebUI +# P.-A. Loizeau, 26 June 2023 + +set(INCLUDE_DIRECTORIES + ${CMAKE_CURRENT_SOURCE_DIR} + ) + +#set(SRCS +# ui_callbacks.cxx +# ) + +list(APPEND HEADERS ui_callbacks.h) + +set(LIBRARY_NAME CbmServicesHistServ) +set(LINKDEF ${LIBRARY_NAME}LinkDef.h ) + +set(PUBLIC_DEPENDENCIES + ROOT::Core + ) + +generate_cbm_library() diff --git a/services/histserv/lib/CbmServicesHistServLinkDef.h b/services/histserv/lib/CbmServicesHistServLinkDef.h new file mode 100644 index 0000000000000000000000000000000000000000..843a69aa136009d78e0401ca8013ff16c8c47de0 --- /dev/null +++ b/services/histserv/lib/CbmServicesHistServLinkDef.h @@ -0,0 +1,11 @@ +/* Copyright (C) 2023 Facility for Antiproton and Ion Research in Europe, Darmstadt + SPDX-License-Identifier: GPL-3.0-only + Authors: Pierre-Alain Loizeau [committer] */ + +#ifdef __CINT__ + +#pragma link off all globals; +#pragma link off all classes; +#pragma link off all functions; + +#endif diff --git a/services/histserv/lib/ui_callbacks.h b/services/histserv/lib/ui_callbacks.h new file mode 100644 index 0000000000000000000000000000000000000000..a658747a76f8da3eb77474a7ae974b5d5b584945 --- /dev/null +++ b/services/histserv/lib/ui_callbacks.h @@ -0,0 +1,12 @@ +/* Copyright (C) 2023 Facility for Antiproton and Ion Research in Europe, Darmstadt + SPDX-License-Identifier: GPL-3.0-only + Authors: Pierre-Alain Loizeau [committer] */ + +#ifndef CBM_SERVICES_HISTSERV_LIB_UI_CALLBACKS_H +#define CBM_SERVICES_HISTSERV_LIB_UI_CALLBACKS_H 1 + +static bool bHistoServerResetHistos = false; +static bool bHistoServerSaveHistos = false; +static bool bHistoServerStop = false; + +#endif /* CBM_SERVICES_HISTSERV_LIB_UI_CALLBACKS_H */