otsdaq  3.10.00
ARTDAQSupervisor.cc
1 
2 
3 #define TRACEMF_USE_VERBATIM 1 // for trace longer path filenames
4 #include "otsdaq/ARTDAQSupervisor/ARTDAQSupervisor.hh"
5 
6 #include "artdaq-core/Utilities/configureMessageFacility.hh"
7 #include "artdaq/BuildInfo/GetPackageBuildInfo.hh"
8 #include "artdaq/DAQdata/Globals.hh"
9 #include "artdaq/ExternalComms/MakeCommanderPlugin.hh"
10 #include "cetlib_except/exception.h"
11 #include "fhiclcpp/make_ParameterSet.h"
12 #include "otsdaq/ARTDAQSupervisor/ARTDAQSupervisorTRACEController.h"
13 
14 #include "artdaq-core/Utilities/ExceptionHandler.hh" /*for artdaq::ExceptionHandler*/
15 
16 #include <boost/exception/all.hpp>
17 #include <boost/filesystem.hpp>
18 
19 #include <signal.h>
20 #include <cerrno>
21 #include <cstring>
22 #include <fstream>
23 #include <regex>
24 
25 #include "otsdaq/ConfigurationInterface/ConfigurationInterface.h"
26 #include "otsdaq/Macros/StringMacros.h"
27 #include "otsdaq/TableCore/TableBase.h"
28 
29 #define OUT_ON_ERR_SIZE 2000 //tail size of output to include on error
30 
31 using namespace ots;
32 
33 XDAQ_INSTANTIATOR_IMPL(ARTDAQSupervisor)
34 
35 const std::string ARTDAQSupervisor::ARTDAQ_SYSVAR_NAMESPACE = "artdaq";
36 
37 #define FAKE_CONFIG_NAME "ots_config"
38 #define DAQINTERFACE_PORT \
39  std::atoi(__ENV__("ARTDAQ_BASE_PORT")) + \
40  (partition_ * std::atoi(__ENV__("ARTDAQ_PORTS_PER_PARTITION")))
41 
42 static ARTDAQSupervisor* instance = nullptr;
43 static std::unordered_map<int, struct sigaction> old_actions =
44  std::unordered_map<int, struct sigaction>();
45 static bool sighandler_init = false;
46 static void signal_handler(int signum)
47 {
48 // Messagefacility may already be gone at this point, TRACE ONLY!
49 #if TRACE_REVNUM < 1459
50  TRACE_STREAMER(TLVL_ERROR, &("ARTDAQsupervisor")[0], 0, 0, 0)
51 #else
52  TRACE_STREAMER(TLVL_ERROR, TLOG2("ARTDAQsupervisor", 0), 0)
53 #endif
54  << "A signal of type " << signum
55  << " was caught by ARTDAQSupervisor. Shutting down DAQInterface, "
56  "then proceeding with default handlers!";
57 
58  if(instance)
59  instance->destroy();
60 
61  sigset_t set;
62  pthread_sigmask(SIG_UNBLOCK, NULL, &set);
63  pthread_sigmask(SIG_UNBLOCK, &set, NULL);
64 
65 #if TRACE_REVNUM < 1459
66  TRACE_STREAMER(TLVL_ERROR, &("ARTDAQsupervisor")[0], 0, 0, 0)
67 #else
68  TRACE_STREAMER(TLVL_ERROR, TLOG2("ARTDAQsupervisor", 0), 0)
69 #endif
70  << "Calling default signal handler";
71  if(signum != SIGUSR2)
72  {
73  sigaction(signum, &old_actions[signum], NULL);
74  kill(getpid(), signum); // Only send signal to self
75  }
76  else
77  {
78  // Send Interrupt signal if parsing SIGUSR2 (i.e. user-defined exception that
79  // should tear down ARTDAQ)
80  sigaction(SIGINT, &old_actions[SIGINT], NULL);
81  kill(getpid(), SIGINT); // Only send signal to self
82  }
83 }
84 
85 static void init_sighandler(ARTDAQSupervisor* inst)
86 {
87  static std::mutex sighandler_mutex;
88  std::unique_lock<std::mutex> lk(sighandler_mutex);
89 
90  if(!sighandler_init)
91  {
92  sighandler_init = true;
93  instance = inst;
94  std::vector<int> signals = {
95  SIGINT,
96  SIGILL,
97  SIGABRT,
98  SIGFPE,
99  SIGSEGV,
100  SIGPIPE,
101  SIGALRM,
102  SIGTERM,
103  SIGUSR2,
104  SIGHUP}; // SIGQUIT is used by art in normal operation
105  for(auto signal : signals)
106  {
107  struct sigaction old_action;
108  sigaction(signal, NULL, &old_action);
109 
110  // If the old handler wasn't SIG_IGN (it's a handler that just
111  // "ignore" the signal)
112  if(old_action.sa_handler != SIG_IGN)
113  {
114  struct sigaction action;
115  action.sa_handler = signal_handler;
116  sigemptyset(&action.sa_mask);
117  for(auto sigblk : signals)
118  {
119  sigaddset(&action.sa_mask, sigblk);
120  }
121  action.sa_flags = 0;
122 
123  // Replace the signal handler of SIGINT with the one described by
124  // new_action
125  sigaction(signal, &action, NULL);
126  old_actions[signal] = old_action;
127  }
128  }
129  }
130 }
131 
132 //==============================================================================
133 ARTDAQSupervisor::ARTDAQSupervisor(xdaq::ApplicationStub* stub)
134  : CoreSupervisorBase(stub)
135  , daqinterface_ptr_(NULL)
136  , partition_(getSupervisorProperty("partition", 0))
137  , daqinterface_state_("notrunning")
138  , runner_thread_(nullptr)
139 {
140  __SUP_COUT__ << "Constructor." << __E__;
141 
142  INIT_MF("." /*directory used is USER_DATA/LOG/.*/);
143  init_sighandler(this);
144 
145  // Only use system Python
146  // unsetenv("PYTHONPATH");
147  // unsetenv("PYTHONHOME");
148 
149  // Write out settings file
150  auto settings_file = __ENV__("DAQINTERFACE_SETTINGS");
151  std::ofstream of(settings_file, std::ios::trunc);
152  const int openErrno = errno; // capture errno immediately after open attempt
153  if(!of.is_open() || of.fail())
154  {
155  __SS__ << "Failed to open DAQINTERFACE_SETTINGS file '" << settings_file
156  << "' for writing: " << strerror(openErrno) << __E__;
157  __SS_THROW__;
158  }
159  std::stringstream o;
160 
161  setenv("DAQINTERFACE_PARTITION_NUMBER", std::to_string(partition_).c_str(), 1);
162  auto logfileName = std::string(__ENV__("OTSDAQ_LOG_DIR")) +
163  "/DAQInteface/DAQInterface_partition" +
164  std::to_string(partition_) + ".log";
165  setenv("DAQINTERFACE_LOGFILE", logfileName.c_str(), 1);
166 
167  o << "log_directory: "
168  << getSupervisorProperty("log_directory", std::string(__ENV__("OTSDAQ_LOG_DIR")))
169  << std::endl;
170 
171  {
172  const std::string record_directory = getSupervisorProperty(
173  "record_directory", ARTDAQTableBase::ARTDAQ_FCL_PATH + "/run_records/");
174  mkdir(record_directory.c_str(), 0755);
175  o << "record_directory: " << record_directory << std::endl;
176  }
177 
178  o << "package_hashes_to_save: "
179  << getSupervisorProperty("package_hashes_to_save", "[artdaq]") << std::endl;
180 
181  o << "spack_root_for_bash_scripts: "
182  << getSupervisorProperty("spack_root_for_bash_scripts",
183  std::string(__ENV__("SPACK_ROOT")))
184  << std::endl;
185  o << "boardreader timeout: " << getSupervisorProperty("boardreader_timeout", 30)
186  << std::endl;
187  o << "eventbuilder timeout: " << getSupervisorProperty("eventbuilder_timeout", 30)
188  << std::endl;
189  o << "datalogger timeout: " << getSupervisorProperty("datalogger_timeout", 30)
190  << std::endl;
191  o << "dispatcher timeout: " << getSupervisorProperty("dispatcher_timeout", 30)
192  << std::endl;
193  // Only put max_fragment_size_bytes into DAQInterface settings file if advanced_memory_usage is disabled
194  if(!getSupervisorProperty("advanced_memory_usage", false))
195  {
196  o << "max_fragment_size_bytes: "
197  << getSupervisorProperty("max_fragment_size_bytes", 1048576) << std::endl;
198  }
199  o << "transfer_plugin_to_use: "
200  << getSupervisorProperty("transfer_plugin_to_use", "TCPSocket") << std::endl;
201  if(getSupervisorProperty("transfer_plugin_from_brs", "") != "")
202  {
203  o << "transfer_plugin_from_brs: "
204  << getSupervisorProperty("transfer_plugin_from_brs", "") << std::endl;
205  }
206  if(getSupervisorProperty("transfer_plugin_from_ebs", "") != "")
207  {
208  o << "transfer_plugin_from_ebs: "
209  << getSupervisorProperty("transfer_plugin_from_ebs", "") << std::endl;
210  }
211  if(getSupervisorProperty("transfer_plugin_from_dls", "") != "")
212  {
213  o << "transfer_plugin_from_dls: "
214  << getSupervisorProperty("transfer_plugin_from_dls", "") << std::endl;
215  }
216  o << "all_events_to_all_dispatchers: " << std::boolalpha
217  << getSupervisorProperty("all_events_to_all_dispatchers", true) << std::endl;
218  if(getSupervisorProperty("data_directory_override", "") != "")
219  {
220  o << "data_directory_override: "
221  << getSupervisorProperty("data_directory_override", "") << std::endl;
222  }
223  o << "max_configurations_to_list: "
224  << getSupervisorProperty("max_configurations_to_list", 10) << std::endl;
225  o << "disable_unique_rootfile_labels: "
226  << getSupervisorProperty("disable_unique_rootfile_labels", false) << std::endl;
227  o << "use_messageviewer: " << std::boolalpha
228  << getSupervisorProperty("use_messageviewer", false) << std::endl;
229  o << "use_messagefacility: " << std::boolalpha
230  << getSupervisorProperty("use_messagefacility", true) << std::endl;
231  o << "fake_messagefacility: " << std::boolalpha
232  << getSupervisorProperty("fake_messagefacility", false) << std::endl;
233  o << "kill_existing_processes: " << std::boolalpha
234  << getSupervisorProperty("kill_existing_processes", true) << std::endl;
235  o << "advanced_memory_usage: " << std::boolalpha
236  << getSupervisorProperty("advanced_memory_usage", false) << std::endl;
237  o << "strict_fragment_id_mode: " << std::boolalpha
238  << getSupervisorProperty("strict_fragment_id_mode", false) << std::endl;
239  o << "disable_private_network_bookkeeping: " << std::boolalpha
240  << getSupervisorProperty("disable_private_network_bookkeeping", false) << std::endl;
241  o << "allowed_processors: "
242  << getSupervisorProperty(
243  "allowed_processors",
244  "0-255") // Note this sets a taskset for ALL processes, on all nodes (ex. "1,2,5-7")
245  << std::endl;
246  if(getSupervisorProperty("partition_label_format", "") !=
247  "") //Add to ARTDAQSupervisor properties partition_label_format: "-P%s"
248  o << "partition_label_format: "
249  << getSupervisorProperty("partition_label_format", "") << std::endl;
250 
251  __COUT_MULTI__(0, o.str());
252 
253  of << o.str();
254  of.close();
255 
256  // destroy current TRACEController and instantiate ARTDAQSupervisorTRACEController
257  if(CorePropertySupervisorBase::theTRACEController_)
258  {
259  __SUP_COUT__ << "Destroying TRACE Controller..." << __E__;
260  delete CorePropertySupervisorBase::
261  theTRACEController_; // destruct current TRACEController
262  CorePropertySupervisorBase::theTRACEController_ = nullptr;
263  }
264  CorePropertySupervisorBase::theTRACEController_ =
266  ((ARTDAQSupervisorTRACEController*)CorePropertySupervisorBase::theTRACEController_)
267  ->setSupervisorPtr(this);
268 
269  __SUP_COUT__ << "Constructed." << __E__;
270 } // end constructor()
271 
272 //==============================================================================
273 ARTDAQSupervisor::~ARTDAQSupervisor(void)
274 {
275  __SUP_COUT__ << "Destructor." << __E__;
276  destroy();
277 
278  __SUP_COUT__ << "Calling Py_Finalize()" << __E__;
279  Py_Finalize();
280 
281  // CorePropertySupervisorBase would destroy, but since it was created here, attempt to destroy
283  {
284  __SUP_COUT__ << "Destroying TRACE Controller..." << __E__;
287  }
288 
289  __SUP_COUT__ << "Destructed." << __E__;
290 } // end destructor()
291 
292 //==============================================================================
293 void ARTDAQSupervisor::destroy(void)
294 {
295  __SUP_COUT__ << "Destroying..." << __E__;
296 
297  if(daqinterface_ptr_ != NULL)
298  {
299  __SUP_COUT__ << "Calling recover transition" << __E__;
300  std::lock_guard<std::recursive_mutex> lk(daqinterface_pythonMutex_);
301 
302  PyObjectGuard pName(PyUnicode_FromString("do_recover"));
303  PyObjectGuard res(
304  PyObject_CallMethodObjArgs(daqinterface_ptr_, pName.get(), NULL));
305 
306  __SUP_COUT__ << "Making sure that correct state has been reached" << __E__;
307  getDAQState_();
308  while(daqinterface_state_ != "stopped")
309  {
310  getDAQState_();
311  __SUP_COUT__ << "State is " << daqinterface_state_
312  << ", waiting 1s and retrying..." << __E__;
313  usleep(1000000);
314  }
315 
316  // Cleanup
317  Py_XDECREF(daqinterface_ptr_);
318  daqinterface_ptr_ = NULL;
319  }
320 
321  __SUP_COUT__ << "Flusing printouts" << __E__;
322 
323  //make sure to flush printouts
324  PyRun_SimpleString(R"(
325 import sys
326 sys.stdout = sys.__stdout__
327 sys.stderr = sys.__stderr__
328 )");
329  // stringIO_out_ and stringIO_err_ may refer to borrowed Python objects
330  // (e.g., via PyDict_GetItemString in the tee-buffer path). Do not DECREF
331  // them here to avoid corrupting their reference counts; treat them as
332  // non-owning pointers and clear them instead.
333  stringIO_out_ = nullptr;
334  stringIO_err_ = nullptr;
335 
336  __SUP_COUT__ << "Thread and garbage cleanup" << __E__;
337  // force python thread cleanup:
338  PyRun_SimpleString(
339  "import threading; [t.join() for t in threading.enumerate() if t is not "
340  "threading.main_thread() and not isinstance(t, threading._DummyThread)]");
341  PyRun_SimpleString("import gc; gc.collect()");
342  // __SUP_COUT__ << "Calling Py_Finalize()" << __E__;
343  // Py_Finalize();
344 
345  __SUP_COUT__ << "Destroyed." << __E__;
346 } // end destroy()
347 
348 //==============================================================================
349 void ARTDAQSupervisor::init(void)
350 {
351  stop_runner_();
352 
353  __SUP_COUT__ << "Initializing..." << __E__;
354  {
355  std::lock_guard<std::recursive_mutex> lk(daqinterface_pythonMutex_);
356 
357  // allSupervisorInfo_.init(getApplicationContext());
358  artdaq::configureMessageFacility("ARTDAQSupervisor");
359  __SUP_COUT__ << "artdaq MF configured." << __E__;
360 
361  // initialization
362  char* daqinterface_dir = getenv("ARTDAQ_DAQINTERFACE_DIR");
363  if(daqinterface_dir == NULL)
364  {
365  __SS__ << "ARTDAQ_DAQINTERFACE_DIR environment variable not set! This "
366  "means that DAQInterface has not been setup!"
367  << __E__;
368  __SUP_SS_THROW__;
369  }
370  else
371  {
372  __SUP_COUT__ << "Initializing Python" << __E__;
373  Py_Initialize();
374 
375  //setup Python output to tee output to stdout/err and to StringIO buffer "tee_buffer"
376  PyRun_SimpleString(
377  "import sys\n"
378  "from io import StringIO\n"
379  "\n"
380  "class TeeOut:\n"
381  " def __init__(self, real, buf):\n"
382  " self.real = real\n"
383  " self.buf = buf\n"
384  " def write(self, data):\n"
385  " self.real.write(data)\n"
386  " self.buf.write(data)\n"
387  " def flush(self):\n"
388  " self.real.flush()\n"
389  " self.buf.flush()\n"
390  "\n"
391  "tee_buffer = StringIO()\n"
392  "sys.stdout = TeeOut(sys.stdout, tee_buffer)\n"
393  "sys.stderr = TeeOut(sys.stderr, tee_buffer)\n");
394 
395  __SUP_COUT__ << "Adding DAQInterface directory to PYTHON_PATH" << __E__;
396  PyObject* sysPath = PySys_GetObject(
397  (char*)"path"); //do NOT DECREF borrowed objects through GetObject!
398  PyObjectGuard programName(PyUnicode_FromString(daqinterface_dir));
399  PyList_Append(sysPath, programName.get());
400 
401  __SUP_COUT__ << "Creating Module name" << __E__;
402  PyObjectGuard pName(PyUnicode_FromString("rc.control.daqinterface"));
403  /* Error checking of pName left out */
404 
405  __SUP_COUT__ << "Importing module" << __E__;
406  PyObjectGuard pModule(PyImport_Import(pName.get()));
407 
408  if(pModule.get() == NULL)
409  {
410  std::string err = capturePyErr("import rc.control.daqinterface");
411  __SS__ << "Failed to load rc.control.daqinterface. Python Exception: "
412  << err << __E__;
413  __SUP_SS_THROW__;
414  }
415  else
416  {
417  __SUP_COUT__ << "Loading python module dictionary" << __E__;
418  PyObject* pDict = PyModule_GetDict(
419  pModule.get()); //do NOT DECREF borrowed objects through GetDict!
420  if(pDict == NULL)
421  {
422  std::string err = capturePyErr("module dict");
423  __SS__ << "Unable to load module dictionary. Python Exception: "
424  << err << __E__;
425  __SUP_SS_THROW__;
426  }
427  else
428  {
429  __SUP_COUT__ << "Getting DAQInterface object pointer" << __E__;
430  PyObject* di_obj_raw = PyDict_GetItemString(
431  pDict, "DAQInterface"); // borrowed reference
432  if(di_obj_raw == NULL)
433  {
434  std::string err = capturePyErr("DAQInterface lookup");
435  __SS__ << "Unable to find 'DAQInterface' in module dictionary. "
436  "Python Exception: "
437  << err << __E__;
438  __SUP_SS_THROW__;
439  }
440  Py_INCREF(di_obj_raw); // convert borrowed reference to owned
441  PyObjectGuard di_obj_ptr(di_obj_raw);
442 
443  __SUP_COUT__ << "Filling out DAQInterface args struct" << __E__;
444  PyObjectGuard pArgs(PyTuple_New(0));
445 
446  PyObjectGuard kwargs(Py_BuildValue("{s:s, s:s, s:i, s:i, s:s, s:s}",
447  "logpath",
448  ".daqint.log",
449  "name",
450  "DAQInterface",
451  "partition_number",
452  partition_,
453  "rpc_port",
454  DAQINTERFACE_PORT,
455  "rpc_host",
456  "localhost",
457  "control_host",
458  "localhost"));
459 
460  __SUP_COUT__ << "Calling DAQInterface Object Constructor" << __E__;
461 
462  // Get sys and io
463  PyObjectGuard sys(PyImport_ImportModule("sys"));
464  PyObjectGuard io(PyImport_ImportModule("io"));
465 
466  if(0)
467  {
468  //------------- redirect stdout to string
469 
470  // Create StringIO objects for stdout and stderr
471  stringIO_out_ = PyObject_CallMethod(io.get(), "StringIO", NULL);
472  stringIO_err_ = PyObject_CallMethod(io.get(), "StringIO", NULL);
473 
474  // Save originals (not needed, since just keep the redirection until daqinterface_ptr_ is destructed)
475  // PyObject* sys_stdout = PyObject_GetAttrString(sys, "stdout");
476  // PyObject* sys_stderr = PyObject_GetAttrString(sys, "stderr");
477 
478  // Redirect
479  PyObject_SetAttrString(sys.get(), "stdout", stringIO_out_);
480  PyObject_SetAttrString(sys.get(), "stderr", stringIO_err_);
481  //------------- end redirect stdout to string
482  }
483  else //capture tee buffer instead so output to console continues
484  {
485  PyObject* mainmod =
486  PyImport_AddModule("__main__"); // borrowed ref
487  PyObject* globals = PyModule_GetDict(mainmod); // borrowed ref
488 
489  stringIO_out_ =
490  PyDict_GetItemString(globals, "tee_buffer"); // borrowed ref
491 
492  // Do not Py_DECREF borrowed references.
493  }
494 
495  daqinterface_ptr_ =
496  PyObject_Call(di_obj_ptr.get(), pArgs.get(), kwargs.get());
497  if(checkPythonError(daqinterface_ptr_))
498  {
499  std::string err = capturePyErr("DAQInterface constructor");
500  __SS__ << "DAQInterface constructor failed. Python Exception: "
501  << err << __E__;
502  __SUP_SS_THROW__;
503  }
504 
505  if(0) //example printout handling
506  {
507  // Force an error
508  PyObjectGuard bad(
509  PyObject_CallMethod(sys.get(), "does_not_exist", NULL));
510  if(!bad.get())
511  PyErr_Print(); // <-- this writes into stringIO_err_, not the terminal
512 
513  // Grab stderr contents
514  PyObjectGuard err_text(
515  PyObject_CallMethod(stringIO_err_, "getvalue", NULL));
516  if(err_text.get())
517  __COUT__ << "Captured stderr:\n"
518  << PyUnicode_AsUTF8(err_text.get()) << "\n";
519  else
520  __COUT__ << "Capture of stderr failed.";
521  } //end example printout handling
522  }
523  }
524  }
525 
526  getDAQState_();
527 
528  // { //attempt to cleanup old artdaq processes DOES NOT WORK because artdaq interface knows it hasn't started
529  // __SUP_COUT__ << "Attempting artdaq stale cleanup..." << __E__;
530  // std::lock_guard<std::recursive_mutex> lk(daqinterface_pythonMutex_);
531  // getDAQState_();
532  // __SUP_COUT__ << "Status before cleanup: " << daqinterface_state_ << __E__;
533 
534  // PyObjectGuard pName(PyUnicode_FromString("do_recover"));
535  // PyObjectGuard res(PyObject_CallMethodObjArgs(daqinterface_ptr_, pName, NULL));
536  // __COUT_MULTI_LBL__(0,captureStderrAndStdout_("do_recover"),"do_recover");
537 
538  // if(res == NULL)
539  // {
540  // std::string err = capturePyErr("do_recover");
541  // __SS__ << "Error with clean up calling do_recover: " << err << __E__;
542  // __SUP_SS_THROW__;
543  // }
544  // getDAQState_();
545  // __SUP_COUT__ << "Status after cleanup: " << daqinterface_state_ << __E__;
546  // __SUP_COUT__ << "cleanup DONE." << __E__;
547  // }
548  }
549  start_runner_();
550 
551  initArtdaqSystemVariables();
552 
553  __SUP_COUT__ << "Initialized." << __E__;
554 } // end init()
555 
556 //==============================================================================
557 void ARTDAQSupervisor::transitionConfiguring(toolbox::Event::Reference /*event*/)
558 {
559  __SUP_COUTT__ << "transitionConfiguring" << __E__;
560 
561  loadArtdaqSystemVariables();
562 
563  // activate the configuration tree (the first iteration)
564  if(RunControlStateMachine::getIterationIndex() == 0 &&
565  RunControlStateMachine::getSubIterationIndex() == 0)
566  {
567  thread_error_message_ = "";
568  thread_progress_bar_.resetProgressBar(0);
569  last_thread_progress_update_ = time(0); // initialize timeout timer
570 
571  CoreSupervisorBase::configureInit();
572 
573  // start configuring thread
574  std::thread(&ARTDAQSupervisor::configuringThread, this).detach();
575 
576  __SUP_COUT__ << "Configuring thread started." << __E__;
577 
578  RunControlStateMachine::
579  indicateIterationWork(); // use Iteration to allow other steps to complete in the system
580  }
581  else // not first time
582  {
583  std::string errorMessage;
584  {
585  std::lock_guard<std::mutex> lock(
586  thread_mutex_); // lock out for remainder of scope
587  errorMessage = thread_error_message_; // theStateMachine_.getErrorMessage();
588  }
589  int progress = thread_progress_bar_.read();
590  __SUP_COUTVS__(2, errorMessage);
591  __SUP_COUTVS__(2, progress);
592  __SUP_COUTVS__(2, thread_progress_bar_.isComplete());
593 
594  // check for done and error messages
595  if(errorMessage == "" && // if no update in 600 seconds, give up
596  time(0) - last_thread_progress_update_ > 600)
597  {
598  __SUP_SS__ << "There has been no update from the configuration thread for "
599  << (time(0) - last_thread_progress_update_)
600  << " seconds, assuming something is wrong and giving up! "
601  << "Last progress received was " << progress << __E__;
602  errorMessage = ss.str();
603  }
604 
605  // Check if any subapp (artdaq component) has entered a Failed state.
606  // This can happen when a component fails during do_config/do_boot while the
607  // configuringThread is blocked waiting for DAQInterface, and would otherwise
608  // cause a 600-second timeout before the error is detected.
609  if(errorMessage == "")
610  {
611  auto subapps = getSubappInfo();
612  for(auto& subapp : subapps)
613  {
614  if(subapp.status == RunControlStateMachine::FAILED_STATE_NAME)
615  {
616  __SUP_SS__ << "Component '" << subapp.name
617  << "' entered Failed state during configuration! "
618  << "(url: " << subapp.url << ")" << __E__;
619  errorMessage = ss.str();
620  __SUP_COUT_ERR__ << "\n" << ss.str();
621  break;
622  }
623  }
624  }
625 
626  if(errorMessage != "")
627  {
628  __SUP_SS__ << "Error was caught in configuring thread: " << errorMessage
629  << __E__;
630  __SUP_COUT_ERR__ << "\n" << ss.str();
631 
632  theStateMachine_.setErrorMessage(ss.str());
633  throw toolbox::fsm::exception::Exception(
634  "Transition Error" /*name*/,
635  ss.str() /* message*/,
636  "CoreSupervisorBase::transitionConfiguring" /*module*/,
637  __LINE__ /*line*/,
638  __FUNCTION__ /*function*/
639  );
640  }
641 
642  if(!thread_progress_bar_.isComplete())
643  {
644  __SUP_COUTT__ << "Not done yet..." << __E__;
645  //attempt to get live view of python output
646  // __COUT_MULTI_LBL__(0, captureStderrAndStdout_("statuscheck"), "statuscheck");
647 
648  RunControlStateMachine::
649  indicateIterationWork(); // use Iteration to allow other steps to complete in the system
650 
651  if(last_thread_progress_read_ != progress)
652  {
653  last_thread_progress_read_ = progress;
654  last_thread_progress_update_ = time(0);
655  }
656 
657  sleep(1 /*seconds*/);
658  }
659  else
660  {
661  __SUP_COUT_INFO__ << "Complete configuring transition!" << __E__;
662  __SUP_COUTV__(getProcessInfo_());
663  }
664  }
665 
666  return;
667 } // end transitionConfiguring()
668 
669 //==============================================================================
670 void ARTDAQSupervisor::configuringThread()
671 try
672 {
673  std::string uid = theConfigurationManager_
674  ->getNode(ConfigurationManager::XDAQ_APPLICATION_TABLE_NAME +
675  "/" + CorePropertySupervisorBase::getSupervisorUID() +
676  "/" + "LinkToSupervisorTable")
677  .getValueAsString();
678 
679  __COUT__ << "Supervisor uid is " << uid << ", getting supervisor table node" << __E__;
680 
681  const std::string mfSubject_ = supervisorClassNoNamespace_ + "-" + uid;
682 
683  ConfigurationTree theSupervisorNode = getSupervisorTableNode();
684 
685  thread_progress_bar_.step();
686 
687  set_thread_message_("ConfigGen");
688 
689  auto info = ARTDAQTableBase::extractARTDAQInfo(
690  theSupervisorNode,
691  false /*getStatusFalseNodes*/,
692  true /*doWriteFHiCL*/,
693  getSupervisorProperty("max_fragment_size_bytes", 8888),
694  getSupervisorProperty("routing_timeout_ms", 1999),
695  getSupervisorProperty("routing_retry_count", 12),
696  &thread_progress_bar_);
697 
698  // Check lists
699  if(info.processes.count(ARTDAQTableBase::ARTDAQAppType::BoardReader) == 0)
700  {
701  __GEN_SS__ << "There must be at least one enabled BoardReader!" << __E__;
702  __GEN_SS_THROW__;
703  }
704  if(info.processes.count(ARTDAQTableBase::ARTDAQAppType::EventBuilder) == 0)
705  {
706  __GEN_SS__ << "There must be at least one enabled EventBuilder!" << __E__;
707  __GEN_SS_THROW__;
708  }
709 
710  thread_progress_bar_.step();
711  set_thread_message_("Writing boot.txt");
712 
713  __GEN_COUT__ << "Writing boot.txt" << __E__;
714 
715  int debugLevel = theSupervisorNode.getNode("DAQInterfaceDebugLevel").getValue<int>();
716  std::string setupScript = theSupervisorNode.getNode("DAQSetupScript").getValue();
717 
718  // Generate boot file content using helper function
719  std::string bootContent =
720  ARTDAQTableBase::getBootFileContentFromInfo(info, setupScript, debugLevel);
721 
722  // Populate label_to_proc_type_map_ (still needed for later)
723  for(auto& builder : info.processes[ARTDAQTableBase::ARTDAQAppType::EventBuilder])
724  label_to_proc_type_map_[builder.label] = "EventBuilder";
725  for(auto& logger : info.processes[ARTDAQTableBase::ARTDAQAppType::DataLogger])
726  label_to_proc_type_map_[logger.label] = "DataLogger";
727  for(auto& dispatcher : info.processes[ARTDAQTableBase::ARTDAQAppType::Dispatcher])
728  label_to_proc_type_map_[dispatcher.label] = "Dispatcher";
729  for(auto& rmanager : info.processes[ARTDAQTableBase::ARTDAQAppType::RoutingManager])
730  label_to_proc_type_map_[rmanager.label] = "RoutingManager";
731 
732  // Write boot.txt file
733  std::ofstream o(ARTDAQTableBase::ARTDAQ_FCL_PATH + "/boot.txt", std::ios::trunc);
734  o << bootContent;
735  o.close();
736 
737  // TODO: To save to runlog, store bootContent in metadata/configuration archive
738  // Example (add when implementing runlog integration):
739  // saveToRunlog("boot.txt", bootContent, run_number);
740 
741  thread_progress_bar_.step();
742  set_thread_message_("Writing Fhicl Files");
743 
744  __GEN_COUT__ << "Building configuration directory" << __E__;
745 
746  boost::system::error_code ignored;
747  boost::filesystem::remove_all(ARTDAQTableBase::ARTDAQ_FCL_PATH + FAKE_CONFIG_NAME,
748  ignored);
749  mkdir((ARTDAQTableBase::ARTDAQ_FCL_PATH + FAKE_CONFIG_NAME).c_str(), 0755);
750 
751  for(auto& reader : info.processes[ARTDAQTableBase::ARTDAQAppType::BoardReader])
752  {
753  symlink(ARTDAQTableBase::getFlatFHICLFilename(
754  ARTDAQTableBase::ARTDAQAppType::BoardReader, reader.label)
755  .c_str(),
756  (ARTDAQTableBase::ARTDAQ_FCL_PATH + FAKE_CONFIG_NAME + "/" +
757  reader.label + ".fcl")
758  .c_str());
759  }
760  for(auto& builder : info.processes[ARTDAQTableBase::ARTDAQAppType::EventBuilder])
761  {
762  symlink(ARTDAQTableBase::getFlatFHICLFilename(
763  ARTDAQTableBase::ARTDAQAppType::EventBuilder, builder.label)
764  .c_str(),
765  (ARTDAQTableBase::ARTDAQ_FCL_PATH + FAKE_CONFIG_NAME + "/" +
766  builder.label + ".fcl")
767  .c_str());
768  }
769  for(auto& logger : info.processes[ARTDAQTableBase::ARTDAQAppType::DataLogger])
770  {
771  symlink(ARTDAQTableBase::getFlatFHICLFilename(
772  ARTDAQTableBase::ARTDAQAppType::DataLogger, logger.label)
773  .c_str(),
774  (ARTDAQTableBase::ARTDAQ_FCL_PATH + FAKE_CONFIG_NAME + "/" +
775  logger.label + ".fcl")
776  .c_str());
777  }
778  for(auto& dispatcher : info.processes[ARTDAQTableBase::ARTDAQAppType::Dispatcher])
779  {
780  symlink(ARTDAQTableBase::getFlatFHICLFilename(
781  ARTDAQTableBase::ARTDAQAppType::Dispatcher, dispatcher.label)
782  .c_str(),
783  (ARTDAQTableBase::ARTDAQ_FCL_PATH + FAKE_CONFIG_NAME + "/" +
784  dispatcher.label + ".fcl")
785  .c_str());
786  }
787  for(auto& rmanager : info.processes[ARTDAQTableBase::ARTDAQAppType::RoutingManager])
788  {
789  symlink(ARTDAQTableBase::getFlatFHICLFilename(
790  ARTDAQTableBase::ARTDAQAppType::RoutingManager, rmanager.label)
791  .c_str(),
792  (ARTDAQTableBase::ARTDAQ_FCL_PATH + FAKE_CONFIG_NAME + "/" +
793  rmanager.label + ".fcl")
794  .c_str());
795  }
796 
797  thread_progress_bar_.step();
798 
799  // Block 1: State check — acquire and release daqinterface_pythonMutex_
800  // so the runner thread and halt transition can interleave between steps
801  {
802  std::lock_guard<std::recursive_mutex> lk(daqinterface_pythonMutex_);
803  getDAQState_();
804  if(daqinterface_state_ != "stopped" && daqinterface_state_ != "")
805  {
806  __GEN_SS__ << "Cannot configure DAQInterface because it is in the wrong state"
807  << " (" << daqinterface_state_ << " != stopped)!" << __E__;
808  __GEN_SS_THROW__
809  }
810 
811  if(daqinterface_ptr_ == nullptr)
812  {
813  __GEN_SS__ << "DAQInterface is not initialized. "
814  "Check earlier Python import/constructor errors (e.g. syntax) "
815  "in DAQInterface."
816  << __E__;
817  __GEN_SS_THROW__;
818  }
819  } // end Block 1 — release daqinterface_pythonMutex_
820 
821  // Block 2: setdaqcomps
822  set_thread_message_("Calling setdaqcomps");
823  __GEN_COUT__ << "Calling setdaqcomps" << __E__;
824  {
825  std::lock_guard<std::recursive_mutex> lk(daqinterface_pythonMutex_);
826 
827  __GEN_COUT__ << "Status before setdaqcomps: " << daqinterface_state_ << __E__;
828 
829  PyObjectGuard pName1(PyUnicode_FromString("setdaqcomps"));
830 
831  PyObjectGuard readerDict(PyDict_New());
832  for(auto& reader : info.processes[ARTDAQTableBase::ARTDAQAppType::BoardReader])
833  {
834  // PyDict_SetItem INCREFs key/value, so use PyObjectGuard to manage references
835  label_to_proc_type_map_[reader.label] = "BoardReader";
836  PyObjectGuard readerName(PyUnicode_FromString(reader.label.c_str()));
837 
838  int list_size = reader.allowed_processors != "" ? 4 : 3;
839 
840  PyObjectGuard readerData(PyList_New(list_size));
841  PyObject* readerHost = PyUnicode_FromString(reader.hostname.c_str());
842  PyObject* readerPort = PyUnicode_FromString("-1");
843  PyObject* readerSubsystem =
844  PyUnicode_FromString(std::to_string(reader.subsystem).c_str());
845  PyList_SetItem(readerData.get(), 0, readerHost);
846  PyList_SetItem(readerData.get(), 1, readerPort);
847  PyList_SetItem(readerData.get(), 2, readerSubsystem);
848  if(reader.allowed_processors != "")
849  {
850  PyObject* readerAllowedProcessors =
851  PyUnicode_FromString(reader.allowed_processors.c_str());
852  PyList_SetItem(readerData.get(), 3, readerAllowedProcessors);
853  }
854  PyDict_SetItem(readerDict.get(), readerName.get(), readerData.get());
855  }
856  PyObjectGuard res1(PyObject_CallMethodObjArgs(
857  daqinterface_ptr_, pName1.get(), readerDict.get(), NULL));
858  __COUT_MULTI_LBL__(0, captureStderrAndStdout_("setdaqcomps"), "setdaqcomps");
859 
860  if(checkPythonError(res1.get()))
861  {
862  std::string err_msg = capturePyErr("setdaqcomps");
863  __GEN_SS__ << "Error calling setdaqcomps: " << err_msg << __E__;
864  __GEN_SS_THROW__;
865  }
866 
867  getDAQState_();
868  __GEN_COUT__ << "Status after setdaqcomps: " << daqinterface_state_ << __E__;
869  } // end Block 2 — release daqinterface_pythonMutex_
870 
871  thread_progress_bar_.step();
872 
873  // Block 3: do_boot (with recover + retry)
874  set_thread_message_("Calling do_boot");
875  __GEN_COUT_INFO__ << "Calling do_boot" << __E__;
876  std::string doBootOutput = "";
877  {
878  std::lock_guard<std::recursive_mutex> lk(daqinterface_pythonMutex_);
879 
880  __GEN_COUT__ << "Status before boot: " << daqinterface_state_ << __E__;
881 
882  // 1. Create Python Strings
883  PyObjectGuard pNameBoot(PyUnicode_FromString("do_boot"));
884  PyObjectGuard pBootArgs(PyUnicode_FromString(
885  (ARTDAQTableBase::ARTDAQ_FCL_PATH + "/boot.txt").c_str()));
886 
887  // 2. First Attempt: Call do_boot
888  PyObjectGuard resBoot1(PyObject_CallMethodObjArgs(
889  daqinterface_ptr_, pNameBoot.get(), pBootArgs.get(), NULL));
890 
891  doBootOutput = captureStderrAndStdout_("do_boot");
892  __COUT_MULTI_LBL__(0, doBootOutput, "do_boot");
893 
894  if(checkPythonError(resBoot1.get()))
895  {
896  // --- FAILURE PATH ---
897 
898  std::string err1 = capturePyErr("do_boot");
899 
900  __GEN_COUT_INFO__ << "Error on first boot attempt: " << err1
901  << ". Recovering and retrying..." << __E__;
902 
903  // B. Attempt 'do_recover'
904  PyObjectGuard pNameRecover(PyUnicode_FromString("do_recover"));
905  PyObjectGuard resRecover(
906  PyObject_CallMethodObjArgs(daqinterface_ptr_, pNameRecover.get(), NULL));
907  __COUT_MULTI_LBL__(0, captureStderrAndStdout_("do_recover"), "do_recover");
908 
909  if(checkPythonError(resRecover.get()))
910  {
911  // Recover failed - Critical Error
912  std::string errRec = capturePyErr("do_recover");
913 
914  std::stringstream oss;
915  oss << "Error calling recover transition!!!! " << errRec;
916  if(doBootOutput.size() > OUT_ON_ERR_SIZE)
917  oss << "... last " << OUT_ON_ERR_SIZE
918  << " chars: " << doBootOutput.substr(doBootOutput.size() - 1000);
919  else
920  oss << doBootOutput;
921 
922  // Clean up original args before throwing
923  __GEN_SS__ << oss.str() << __E__;
924  __GEN_SS_THROW__;
925  }
926 
927  // C. Retry 'do_boot'
928  thread_progress_bar_.step();
929  set_thread_message_("Calling do_boot (retry)");
930  __GEN_COUT_INFO__ << "Calling do_boot again" << __E__;
931 
932  // Reuse pNameBoot and pBootArgs (valid until end of scope)
933  PyObjectGuard resBoot2(PyObject_CallMethodObjArgs(
934  daqinterface_ptr_, pNameBoot.get(), pBootArgs.get(), NULL));
935 
936  doBootOutput = captureStderrAndStdout_("do_boot (retry)");
937  __COUT_MULTI_LBL__(0, doBootOutput, "do_boot (retry)");
938 
939  if(checkPythonError(resBoot2.get()))
940  {
941  // Second boot failed
942  std::string err2 = capturePyErr("do_boot retry");
943 
944  std::stringstream oss;
945  oss << "Error calling boot transition (2nd try): " << err2;
946  if(doBootOutput.size() > OUT_ON_ERR_SIZE)
947  oss << "... last " << OUT_ON_ERR_SIZE
948  << " chars: " << doBootOutput.substr(doBootOutput.size() - 1000);
949  else
950  oss << doBootOutput;
951 
952  __GEN_SS__ << oss.str() << __E__;
953  __GEN_SS_THROW__;
954  }
955  }
956 
957  getDAQState_();
958  if(daqinterface_state_ != "booted")
959  {
960  std::cout << "Do boot output on error: \n" << doBootOutput << __E__;
961  __GEN_SS__ << "DAQInterface boot transition failed! "
962  << "Status after boot attempt: " << daqinterface_state_ << __E__;
963 
964  if(doBootOutput.size() > OUT_ON_ERR_SIZE) //last OUT_ON_ERR_SIZE chars only
965  ss << "... last " << OUT_ON_ERR_SIZE
966  << " characters: " << doBootOutput.substr(doBootOutput.size() - 1000);
967  else
968  ss << doBootOutput;
969  __GEN_SS_THROW__;
970  }
971  __GEN_COUT__ << "Status after boot: " << daqinterface_state_ << __E__;
972  } // end Block 3 — release daqinterface_pythonMutex_
973 
974  thread_progress_bar_.step();
975 
976  // Block 4: do_config
977  set_thread_message_("Calling do_config");
978  __GEN_COUT_INFO__ << "Calling do_config" << __E__;
979  std::string doConfigOutput = "";
980  {
981  std::lock_guard<std::recursive_mutex> lk(daqinterface_pythonMutex_);
982 
983  __GEN_COUT__ << "Status before config: " << daqinterface_state_ << __E__;
984 
985  { //do_config call
986  // RAII wrapper for Python objects to ensure cleanup even on exception
987 
988  PyObjectGuard pName3(PyUnicode_FromString("do_config"));
989  // 2. Create the argument - list containing config name: ["my_config"]
990  PyObjectGuard pArg(Py_BuildValue("[s]", FAKE_CONFIG_NAME));
991 
992  // 3. Call the method
993  PyObjectGuard res3(PyObject_CallMethodObjArgs(
994  daqinterface_ptr_, pName3.get(), pArg.get(), NULL));
995 
996  // 4. Check for errors FIRST before capturing output (which might clear error state)
997  if(checkPythonError(res3.get()))
998  {
999  // Get the error message before doing anything else
1000  std::string err = capturePyErr("do_config");
1001 
1002  // Now capture output for diagnostics
1003  doConfigOutput = captureStderrAndStdout_("do_config");
1004 
1005  __GEN_SS__ << "Error calling config transition: " << err << __E__;
1006  __GEN_SS_THROW__;
1007  }
1008 
1009  // 5. Success path - capture output
1010  doConfigOutput = captureStderrAndStdout_("do_config");
1011  __COUT_MULTI_LBL__(0, doConfigOutput, "do_config");
1012 
1013  // 6. Success Handling (Safe conversion to string)
1014  // We use PyObject_Str to safely convert any return type (None, Int, String) to text
1015  PyObjectGuard strRes(PyObject_Str(res3.get()));
1016  const char* res_cstr = "";
1017  if(strRes.get())
1018  {
1019  res_cstr = PyUnicode_AsUTF8(strRes.get());
1020  }
1021 
1022  __SUP_COUTT__ << "do_config result=" << (res_cstr ? res_cstr : "N/A")
1023  << __E__;
1024  } //end do_config call
1025 
1026  getDAQState_();
1027  if(daqinterface_state_ != "ready")
1028  {
1029  __GEN_SS__ << "DAQInterface config transition failed!" << __E__
1030  << "Supervisor state: \"" << daqinterface_state_
1031  << "\" != \"ready\" " << __E__;
1032  auto doConfigOutput_recover_i =
1033  doConfigOutput.find("RECOVER transition underway");
1034  if(doConfigOutput_recover_i == std::string::npos)
1035  ss << doConfigOutput;
1036  else if(doConfigOutput_recover_i >
1037  OUT_ON_ERR_SIZE) //last OUT_ON_ERR_SIZE chars only
1038  ss << "... tail of " << OUT_ON_ERR_SIZE << " characters before recovery: "
1039  << doConfigOutput.substr(
1040  doConfigOutput_recover_i - OUT_ON_ERR_SIZE +
1041  std::string("RECOVER transition underway").size(),
1042  OUT_ON_ERR_SIZE);
1043  else
1044  ss << doConfigOutput.substr(
1045  0,
1046  doConfigOutput_recover_i +
1047  std::string("RECOVER transition underway").size());
1048  __GEN_SS_THROW__;
1049  }
1050  __GEN_COUT__ << "Status after config: " << daqinterface_state_ << __E__;
1051  } // end Block 4 — release daqinterface_pythonMutex_
1052 
1053  thread_progress_bar_.complete();
1054  set_thread_message_("Configured");
1055  __GEN_COUT_INFO__ << "Configured." << __E__;
1056 
1057 } // end configuringThread()
1058 catch(const std::runtime_error& e)
1059 {
1060  set_thread_message_("ERROR");
1061  __SS__ << "Error was caught while configuring: " << e.what() << __E__;
1062  __COUT_ERR__ << "\n" << ss.str();
1063  std::lock_guard<std::mutex> lock(thread_mutex_); // lock out for remainder of scope
1064  thread_error_message_ = ss.str();
1065 }
1066 catch(...)
1067 {
1068  set_thread_message_("ERROR");
1069  __SS__ << "Unknown error was caught while configuring. Please checked the logs."
1070  << __E__;
1071  __COUT_ERR__ << "\n" << ss.str();
1072 
1073  artdaq::ExceptionHandler(artdaq::ExceptionHandlerRethrow::no, ss.str());
1074 
1075  std::lock_guard<std::mutex> lock(thread_mutex_); // lock out for remainder of scope
1076  thread_error_message_ = ss.str();
1077 } // end configuringThread() error handling
1078 
1079 //==============================================================================
1080 void ARTDAQSupervisor::transitionHalting(toolbox::Event::Reference /*event*/)
1081 try
1082 {
1083  set_thread_message_("Halting");
1084  __SUP_COUT__ << "Halting..." << __E__;
1085 
1086  int tries = 0;
1087  while(tries++ < 5)
1088  {
1089  std::unique_lock<std::recursive_mutex> lk(daqinterface_pythonMutex_,
1090  std::try_to_lock);
1091  if(!lk.owns_lock()) //if lock not availabe, just report last status
1092  {
1093  __COUTS__(50) << "Do not have python lock for halt. tries=" << tries << __E__;
1094  sleep(1);
1095  continue;
1096  }
1097  __COUTS__(50) << "Have python lock!" << __E__;
1098 
1099  // std::lock_guard<std::recursive_mutex> lk(daqinterface_pythonMutex_);
1100  getDAQState_();
1101  __SUP_COUT__ << "Status before halt: " << daqinterface_state_ << __E__;
1102 
1103  if(daqinterface_state_ == "running")
1104  {
1105  // First stop before halting
1106  PyObjectGuard pName(PyUnicode_FromString("do_stop_running"));
1107  PyObjectGuard res(
1108  PyObject_CallMethodObjArgs(daqinterface_ptr_, pName.get(), NULL));
1109  __COUT_MULTI_LBL__(
1110  0, captureStderrAndStdout_("do_stop_running"), "do_stop_running");
1111 
1112  if(res.get() == NULL)
1113  {
1114  std::string err = capturePyErr();
1115  __SS__ << "Error calling DAQ Interface stop transition: " << err
1116  << __E__;
1117  __SUP_SS_THROW__;
1118  }
1119  }
1120 
1121  // If DAQInterface is already stopped (e.g. after enteringError ran do_recover),
1122  // there are no artdaq processes to send Shutdown to — skip do_command.
1123  if(daqinterface_state_ == "stopped" || daqinterface_state_ == "")
1124  {
1125  __SUP_COUT__ << "DAQInterface already stopped, skipping Shutdown command."
1126  << __E__;
1127  }
1128  else
1129  {
1130  PyObjectGuard pName(PyUnicode_FromString("do_command"));
1131  PyObjectGuard pArg(PyUnicode_FromString("Shutdown"));
1132  PyObjectGuard res(PyObject_CallMethodObjArgs(
1133  daqinterface_ptr_, pName.get(), pArg.get(), NULL));
1134  __COUT_MULTI_LBL__(
1135  0, captureStderrAndStdout_("do_command Shutdown"), "do_command Shutdown");
1136 
1137  if(checkPythonError(res.get()))
1138  {
1139  std::string err = capturePyErr("do_command Shutdown");
1140  __SS__ << "Error calling DAQ Interface halt transition: " << err << __E__;
1141  __SUP_SS_THROW__;
1142  }
1143  }
1144 
1145  getDAQState_();
1146  __SUP_COUT__ << "Status after halt: " << daqinterface_state_ << __E__;
1147  break;
1148  } //end retry loop
1149 
1150  if(tries >= 5)
1151  {
1152  __SUP_SS__ << "Failed to acquire python lock for halting after " << tries
1153  << " tries, giving up! Is it possible the configure thread is stuck?"
1154  << __E__;
1155  __SUP_SS_THROW__;
1156  }
1157 
1158  __SUP_COUT__ << "Halted." << __E__;
1159  set_thread_message_("Halted");
1160 } // end transitionHalting()
1161 catch(const std::runtime_error& e)
1162 {
1163  const std::string transitionName = "Halting";
1164  // if halting from Failed state, then ignore errors
1165  if(theStateMachine_.getProvenanceStateName() ==
1166  RunControlStateMachine::FAILED_STATE_NAME ||
1167  theStateMachine_.getProvenanceStateName() ==
1168  RunControlStateMachine::HALTED_STATE_NAME)
1169  {
1170  __SUP_COUT_INFO__ << "Error was caught while halting (but ignoring because "
1171  "previous state was '"
1172  << RunControlStateMachine::FAILED_STATE_NAME
1173  << "'): " << e.what() << __E__;
1174  }
1175  else // if not previously in Failed state, then fail
1176  {
1177  __SUP_SS__ << "Error was caught while " << transitionName << ": " << e.what()
1178  << __E__;
1179  __SUP_COUT_ERR__ << "\n" << ss.str();
1180  theStateMachine_.setErrorMessage(ss.str());
1181  throw toolbox::fsm::exception::Exception(
1182  "Transition Error" /*name*/,
1183  ss.str() /* message*/,
1184  "ARTDAQSupervisorBase::transition" + transitionName /*module*/,
1185  __LINE__ /*line*/,
1186  __FUNCTION__ /*function*/
1187  );
1188  }
1189 } // end transitionHalting() std::runtime_error exception handling
1190 catch(...)
1191 {
1192  const std::string transitionName = "Halting";
1193  // if halting from Failed state, then ignore errors
1194  if(theStateMachine_.getProvenanceStateName() ==
1195  RunControlStateMachine::FAILED_STATE_NAME ||
1196  theStateMachine_.getProvenanceStateName() ==
1197  RunControlStateMachine::HALTED_STATE_NAME)
1198  {
1199  __SUP_COUT_INFO__ << "Unknown error was caught while halting (but ignoring "
1200  "because previous state was '"
1201  << RunControlStateMachine::FAILED_STATE_NAME << "')." << __E__;
1202  }
1203  else // if not previously in Failed state, then fail
1204  {
1205  __SUP_SS__ << "Unknown error was caught while " << transitionName
1206  << ". Please checked the logs." << __E__;
1207  __SUP_COUT_ERR__ << "\n" << ss.str();
1208  theStateMachine_.setErrorMessage(ss.str());
1209 
1210  artdaq::ExceptionHandler(artdaq::ExceptionHandlerRethrow::no, ss.str());
1211 
1212  throw toolbox::fsm::exception::Exception(
1213  "Transition Error" /*name*/,
1214  ss.str() /* message*/,
1215  "ARTDAQSupervisorBase::transition" + transitionName /*module*/,
1216  __LINE__ /*line*/,
1217  __FUNCTION__ /*function*/
1218  );
1219  }
1220 } // end transitionHalting() exception handling
1221 
1222 //==============================================================================
1223 void ARTDAQSupervisor::transitionInitializing(toolbox::Event::Reference /*event*/)
1224 try
1225 {
1226  set_thread_message_("Initializing");
1227  __SUP_COUT__ << "Initializing..." << __E__;
1228  init();
1229  __SUP_COUT__ << "Initialized." << __E__;
1230  set_thread_message_("Initialized");
1231 } // end transitionInitializing()
1232 catch(const std::runtime_error& e)
1233 {
1234  __SS__ << "Error was caught while Initializing: " << e.what() << __E__;
1235  __SS_THROW__;
1236 }
1237 catch(...)
1238 {
1239  __SS__ << "Unknown error was caught while Initializing. Please checked the logs."
1240  << __E__;
1241  artdaq::ExceptionHandler(artdaq::ExceptionHandlerRethrow::no, ss.str());
1242  __SS_THROW__;
1243 } // end transitionInitializing() error handling
1244 
1245 //==============================================================================
1246 void ARTDAQSupervisor::transitionPausing(toolbox::Event::Reference /*event*/)
1247 try
1248 {
1249  set_thread_message_("Pausing");
1250  __SUP_COUT__ << "Pausing..." << __E__;
1251  std::lock_guard<std::recursive_mutex> lk(daqinterface_pythonMutex_);
1252 
1253  getDAQState_();
1254  __SUP_COUT__ << "Status before pause: " << daqinterface_state_ << __E__;
1255 
1256  PyObjectGuard pName(PyUnicode_FromString("do_command"));
1257  PyObjectGuard pArg(PyUnicode_FromString("Pause"));
1258  PyObjectGuard res(
1259  PyObject_CallMethodObjArgs(daqinterface_ptr_, pName.get(), pArg.get(), NULL));
1260  __COUT_MULTI_LBL__(
1261  0, captureStderrAndStdout_("do_command Pause"), "do_command Pause");
1262 
1263  if(checkPythonError(res.get()))
1264  {
1265  std::string err = capturePyErr("do_command Pause");
1266  __SS__ << "Error calling DAQ Interface Pause transition: " << err << __E__;
1267  __SUP_SS_THROW__;
1268  }
1269 
1270  getDAQState_();
1271  __SUP_COUT__ << "Status after pause: " << daqinterface_state_ << __E__;
1272 
1273  __SUP_COUT__ << "Paused." << __E__;
1274  set_thread_message_("Paused");
1275 } // end transitionPausing()
1276 catch(const std::runtime_error& e)
1277 {
1278  __SS__ << "Error was caught while Pausing: " << e.what() << __E__;
1279  __SS_THROW__;
1280 }
1281 catch(...)
1282 {
1283  __SS__ << "Unknown error was caught while Pausing. Please checked the logs." << __E__;
1284  artdaq::ExceptionHandler(artdaq::ExceptionHandlerRethrow::no, ss.str());
1285  __SS_THROW__;
1286 } // end transitionPausing() error handling
1287 
1288 //==============================================================================
1289 void ARTDAQSupervisor::transitionResuming(toolbox::Event::Reference /*event*/)
1290 try
1291 {
1292  set_thread_message_("Resuming");
1293  __SUP_COUT__ << "Resuming..." << __E__;
1294  std::lock_guard<std::recursive_mutex> lk(daqinterface_pythonMutex_);
1295 
1296  getDAQState_();
1297  __SUP_COUT__ << "Status before resume: " << daqinterface_state_ << __E__;
1298  PyObjectGuard pName(PyUnicode_FromString("do_command"));
1299  PyObjectGuard pArg(PyUnicode_FromString("Resume"));
1300  PyObjectGuard res(
1301  PyObject_CallMethodObjArgs(daqinterface_ptr_, pName.get(), pArg.get(), NULL));
1302  __COUT_MULTI_LBL__(
1303  0, captureStderrAndStdout_("do_command Resume"), "do_command Resume");
1304 
1305  if(checkPythonError(res.get()))
1306  {
1307  std::string err = capturePyErr("do_command Resume");
1308  __SS__ << "Error calling DAQ Interface Resume transition: " << err << __E__;
1309  __SUP_SS_THROW__;
1310  }
1311 
1312  getDAQState_();
1313  __SUP_COUT__ << "Status after resume: " << daqinterface_state_ << __E__;
1314  __SUP_COUT__ << "Resumed." << __E__;
1315  set_thread_message_("Resumed");
1316 } // end transitionResuming()
1317 catch(const std::runtime_error& e)
1318 {
1319  __SS__ << "Error was caught while Resuming: " << e.what() << __E__;
1320  __SS_THROW__;
1321 }
1322 catch(...)
1323 {
1324  __SS__ << "Unknown error was caught while Resuming. Please checked the logs."
1325  << __E__;
1326  artdaq::ExceptionHandler(artdaq::ExceptionHandlerRethrow::no, ss.str());
1327  __SS_THROW__;
1328 } // end transitionResuming() error handling
1329 
1330 //==============================================================================
1331 void ARTDAQSupervisor::transitionStarting(toolbox::Event::Reference /*event*/)
1332 try
1333 {
1334  __SUP_COUT__ << "transitionStarting" << __E__;
1335 
1336  // Synchronized start sequence:
1337  // Iteration 0: idle — let DTCs SoftReset before launching artdaq.
1338  // Iteration 1: launch artdaq (do_start_running); block iteration advance
1339  // until complete so DTCs don't SoftReset while artdaq is starting.
1340  // Iteration 2+: idle — DTCs do post-artdaq SoftReset, then CFO launches run plan.
1341 
1342  const unsigned int startIteration = RunControlStateMachine::getIterationIndex();
1343 
1344  if(startIteration == 0)
1345  {
1346  // Step 0: idle — let DTCs SoftReset before we launch artdaq
1347  __SUP_COUT_INFO__ << "Step 0: idle, waiting for DTCs to SoftReset." << __E__;
1348  RunControlStateMachine::indicateIterationWork();
1349  return;
1350  }
1351 
1352  if(startIteration == 1 && RunControlStateMachine::getSubIterationIndex() == 0)
1353  {
1354  // Step 1: launch artdaq do_start_running
1355  thread_error_message_ = "";
1356  thread_progress_bar_.resetProgressBar(0);
1357  last_thread_progress_update_ = time(0);
1358 
1359  std::thread(&ARTDAQSupervisor::startingThread, this).detach();
1360 
1361  __SUP_COUT_INFO__ << "Step 1: artdaq starting thread launched." << __E__;
1362 
1363  // Sub-iterate to poll thread; the Gateway's broadcast thread stays busy
1364  // with this supervisor's sub-iteration loop, blocking global iteration
1365  // advance until artdaq is done.
1366  RunControlStateMachine::indicateSubIterationWork();
1367  return;
1368  }
1369 
1370  if(startIteration == 1) // sub-iteration > 0: poll the starting thread
1371  {
1372  std::string errorMessage;
1373  {
1374  std::lock_guard<std::mutex> lock(
1375  thread_mutex_); // lock out for remainder of scope
1376  errorMessage = thread_error_message_; // theStateMachine_.getErrorMessage();
1377  }
1378  int progress = thread_progress_bar_.read();
1379  __SUP_COUTV__(errorMessage);
1380  __SUP_COUTV__(progress);
1381  __SUP_COUTV__(thread_progress_bar_.isComplete());
1382 
1383  if(errorMessage == "" && time(0) - last_thread_progress_update_ > 600)
1384  {
1385  __SUP_SS__ << "There has been no update from the start thread for "
1386  << (time(0) - last_thread_progress_update_)
1387  << " seconds, assuming something is wrong and giving up! "
1388  << "Last progress received was " << progress << __E__;
1389  errorMessage = ss.str();
1390  }
1391 
1392  if(errorMessage != "")
1393  {
1394  __SUP_SS__ << "Error was caught in starting thread: " << errorMessage
1395  << __E__;
1396  __SUP_COUT_ERR__ << "\n" << ss.str();
1397 
1398  theStateMachine_.setErrorMessage(ss.str());
1399  throw toolbox::fsm::exception::Exception(
1400  "Transition Error" /*name*/,
1401  ss.str() /* message*/,
1402  "CoreSupervisorBase::transitionStarting" /*module*/,
1403  __LINE__ /*line*/,
1404  __FUNCTION__ /*function*/
1405  );
1406  }
1407 
1408  if(!thread_progress_bar_.isComplete())
1409  {
1410  __SUP_COUT__ << "Step 1: artdaq not done yet..." << __E__;
1411 
1412  RunControlStateMachine::indicateSubIterationWork();
1413 
1414  if(last_thread_progress_read_ != progress)
1415  {
1416  last_thread_progress_read_ = progress;
1417  last_thread_progress_update_ = time(0);
1418  }
1419 
1420  sleep(1 /*seconds*/);
1421  }
1422  else
1423  {
1424  // Thread done — stop sub-iterating. The broadcast thread returns,
1425  // and the Gateway sees this supervisor needs another iteration
1426  // (indicateIterationWork) to advance to iteration 2+.
1427  __SUP_COUT_INFO__ << "Step 1: artdaq starting transition completed!" << __E__;
1428  __SUP_COUTV__(getProcessInfo_());
1429  RunControlStateMachine::indicateIterationWork();
1430  }
1431 
1432  return;
1433  }
1434 
1435  // Iterations 2+: idle while DTCs do post-artdaq SoftReset and CFO launches run plan.
1436  return;
1437 
1438 } // end transitionStarting()
1439 catch(const std::runtime_error& e)
1440 {
1441  __SS__ << "Error was caught while Starting: " << e.what() << __E__;
1442  __SS_THROW__;
1443 }
1444 catch(...)
1445 {
1446  __SS__ << "Unknown error was caught while Starting. Please checked the logs."
1447  << __E__;
1448  artdaq::ExceptionHandler(artdaq::ExceptionHandlerRethrow::no, ss.str());
1449  __SS_THROW__;
1450 } // end transitionStarting() error handling
1451 
1452 //==============================================================================
1453 void ARTDAQSupervisor::startingThread()
1454 try
1455 {
1456  std::string uid = theConfigurationManager_
1457  ->getNode(ConfigurationManager::XDAQ_APPLICATION_TABLE_NAME +
1458  "/" + CorePropertySupervisorBase::getSupervisorUID() +
1459  "/" + "LinkToSupervisorTable")
1460  .getValueAsString();
1461 
1462  __COUT__ << "Supervisor uid is " << uid << ", getting supervisor table node" << __E__;
1463  const std::string mfSubject_ = supervisorClassNoNamespace_ + "-" + uid;
1464  __GEN_COUT__ << "Starting..." << __E__;
1465  set_thread_message_("Starting");
1466 
1467  thread_progress_bar_.step();
1468  stop_runner_();
1469  {
1470  std::lock_guard<std::recursive_mutex> lk(daqinterface_pythonMutex_);
1471  getDAQState_();
1472  __GEN_COUT__ << "Status before start: " << daqinterface_state_ << __E__;
1473  auto runNumber = SOAPUtilities::translate(theStateMachine_.getCurrentMessage())
1474  .getParameters()
1475  .getValue("RunNumber");
1476 
1477  thread_progress_bar_.step();
1478 
1479  __GEN_COUT_INFO__ << "Calling do_start_running" << __E__;
1480  PyObjectGuard pName(PyUnicode_FromString("do_start_running"));
1481  int run_number = std::stoi(runNumber);
1482  PyObjectGuard pStateArgs(PyLong_FromLong(run_number));
1483  PyObjectGuard res(PyObject_CallMethodObjArgs(
1484  daqinterface_ptr_, pName.get(), pStateArgs.get(), NULL));
1485  std::string doStartOutput;
1486 
1487  thread_progress_bar_.step();
1488 
1489  if(checkPythonError(res.get()))
1490  {
1491  std::string err = capturePyErr("do_start_running");
1492  doStartOutput = captureStderrAndStdout_("do_start_running");
1493  __SS__ << "Error calling start transition: " << err << __E__;
1494  if(doStartOutput.size() > OUT_ON_ERR_SIZE) //last OUT_ON_ERR_SIZE chars only
1495  ss << "... last " << OUT_ON_ERR_SIZE << " characters: "
1496  << doStartOutput.substr(doStartOutput.size() - OUT_ON_ERR_SIZE);
1497  else
1498  ss << doStartOutput;
1499  __GEN_SS_THROW__;
1500  }
1501 
1502  doStartOutput = captureStderrAndStdout_("do_start_running");
1503  __COUT_MULTI_LBL__(0, doStartOutput, "do_start_running");
1504  getDAQState_();
1505 
1506  thread_progress_bar_.step();
1507 
1508  __GEN_COUT__ << "Status after start: " << daqinterface_state_ << __E__;
1509  if(daqinterface_state_ != "running")
1510  {
1511  __SS__ << "DAQInterface start transition failed!" << __E__
1512  << "DAQInterface state: \"" << daqinterface_state_
1513  << "\" != \"running\" " << __E__;
1514  if(doStartOutput.size() > OUT_ON_ERR_SIZE) //last OUT_ON_ERR_SIZE chars only
1515  ss << "... last " << OUT_ON_ERR_SIZE << " characters: "
1516  << doStartOutput.substr(doStartOutput.size() - OUT_ON_ERR_SIZE);
1517  else
1518  ss << doStartOutput;
1519  __GEN_SS_THROW__;
1520  }
1521 
1522  thread_progress_bar_.step();
1523  }
1524  start_runner_();
1525  set_thread_message_("Started");
1526  thread_progress_bar_.step();
1527 
1528  __GEN_COUT_INFO__ << "Started." << __E__;
1529  thread_progress_bar_.complete();
1530 
1531 } // end startingThread()
1532 catch(const std::runtime_error& e)
1533 {
1534  __SS__ << "Error was caught while Starting: " << e.what() << __E__;
1535  __COUT_ERR__ << "\n" << ss.str();
1536  std::lock_guard<std::mutex> lock(thread_mutex_); // lock out for remainder of scope
1537  thread_error_message_ = ss.str();
1538 }
1539 catch(...)
1540 {
1541  __SS__ << "Unknown error was caught while Starting. Please checked the logs."
1542  << __E__;
1543  __COUT_ERR__ << "\n" << ss.str();
1544 
1545  artdaq::ExceptionHandler(artdaq::ExceptionHandlerRethrow::no, ss.str());
1546 
1547  std::lock_guard<std::mutex> lock(thread_mutex_); // lock out for remainder of scope
1548  thread_error_message_ = ss.str();
1549 } // end startingThread() error handling
1550 
1551 //==============================================================================
1552 void ARTDAQSupervisor::transitionStopping(toolbox::Event::Reference /*event*/)
1553 try
1554 {
1555  __SUP_COUT__ << "Stopping..." << __E__;
1556  set_thread_message_("Stopping");
1557  std::lock_guard<std::recursive_mutex> lk(daqinterface_pythonMutex_);
1558  getDAQState_();
1559  __SUP_COUT__ << "Status before stop: " << daqinterface_state_ << __E__;
1560  PyObjectGuard pName(PyUnicode_FromString("do_stop_running"));
1561  PyObjectGuard res(PyObject_CallMethodObjArgs(daqinterface_ptr_, pName.get(), NULL));
1562  __COUT_MULTI_LBL__(0, captureStderrAndStdout_("do_stop_running"), "do_stop_running");
1563 
1564  if(checkPythonError(res.get()))
1565  {
1566  std::string err = capturePyErr("do_stop_running");
1567  __SS__ << "Error calling DAQ Interface stop transition: " << err << __E__;
1568  __SUP_SS_THROW__;
1569  }
1570  getDAQState_();
1571  __SUP_COUT__ << "Status after stop: " << daqinterface_state_ << __E__;
1572  __SUP_COUT__ << "Stopped." << __E__;
1573  set_thread_message_("Stopped");
1574 } // end transitionStopping()
1575 catch(const std::runtime_error& e)
1576 {
1577  __SS__ << "Error was caught while Stopping: " << e.what() << __E__;
1578  __SS_THROW__;
1579 }
1580 catch(...)
1581 {
1582  __SS__ << "Unknown error was caught while Stopping. Please checked the logs."
1583  << __E__;
1584  artdaq::ExceptionHandler(artdaq::ExceptionHandlerRethrow::no, ss.str());
1585  __SS_THROW__;
1586 } // end transitionStopping() error handling
1587 
1588 //==============================================================================
1589 void ots::ARTDAQSupervisor::enteringError(toolbox::Event::Reference /*event*/)
1590 {
1591  __SUP_COUT__ << "Entering error recovery state" << __E__;
1592  std::lock_guard<std::recursive_mutex> lk(daqinterface_pythonMutex_);
1593  getDAQState_();
1594  __SUP_COUT__ << "Status before error: " << daqinterface_state_ << __E__;
1595 
1596  PyObjectGuard pName(PyUnicode_FromString("do_recover"));
1597  PyObjectGuard res(PyObject_CallMethodObjArgs(daqinterface_ptr_, pName.get(), NULL));
1598  __COUT_MULTI_LBL__(0, captureStderrAndStdout_("do_recover"), "do_recover");
1599 
1600  if(checkPythonError(res.get()))
1601  {
1602  std::string err = capturePyErr("do_recover");
1603  //do not throw exception when entering error, because failing DAQ interface could be the reason for error in first place
1604  __SUP_COUT_WARN__ << "Error calling DAQ Interface recover transition: " << err
1605  << __E__;
1606  return;
1607  }
1608 
1609  getDAQState_();
1610  __SUP_COUT__ << "Status after error: " << daqinterface_state_ << __E__;
1611  __SUP_COUT__ << "EnteringError DONE." << __E__;
1612 
1613 } // end enteringError()
1614 
1615 std::vector<SupervisorInfo::SubappInfo> ots::ARTDAQSupervisor::getSubappInfo(void)
1616 {
1617  auto apps = getAndParseProcessInfo_();
1618 
1619  std::map<int, SupervisorInfo::SubappInfo> subapp_infos;
1620  for(auto& app : apps)
1621  {
1623 
1624  info.name = app.label;
1625  info.detail = "Rank " + std::to_string(app.rank) + ", subsystem " +
1626  std::to_string(app.subsystem);
1627  info.lastStatusTime = time(0);
1628  info.progress = 100;
1629  info.status = artdaqStateToOtsState(app.state);
1630  info.url = "http://" + app.host + ":" + std::to_string(app.port) + "/RPC2";
1631  info.class_name = "ARTDAQ " + labelToProcType_(app.label);
1632 
1633  subapp_infos[app.rank] = info;
1634  }
1635 
1636  std::vector<SupervisorInfo::SubappInfo> output;
1637  for(auto& [rank, info] : subapp_infos)
1638  {
1639  output.push_back(info);
1640  }
1641  return output;
1642 } //end getSubappInfo()
1643 
1644 //==============================================================================
1645 // Helper function to check if a Python call failed
1646 // Returns true if there was an error (result is NULL or PyErr_Occurred)
1647 // NOTE: Does NOT clear the Python error state - caller should call capturePyErr() to fetch it
1648 bool ots::ARTDAQSupervisor::checkPythonError(PyObject* result)
1649 {
1650  if(result == NULL || PyErr_Occurred())
1651  {
1652  // Assume result is cleaned up by its PyObjectGuard if needed
1653 
1654  // Note: We keep the Python error state so caller can extract the message with capturePyErr()
1655  return true; // Error occurred
1656  }
1657  return false; // No error
1658 } //end checkPythonError()
1659 
1660 //==============================================================================
1661 std::string ots::ARTDAQSupervisor::capturePyErr(std::string label /* = "" */)
1662 {
1663  std::string err_msg = "Unknown Python Error";
1664  PyObject * pType, *pValue, *pTraceback;
1665  PyErr_Fetch(&pType, &pValue, &pTraceback);
1666  PyErr_NormalizeException(&pType, &pValue, &pTraceback);
1667 
1668  if(pType != NULL)
1669  {
1670  // Format the full traceback like Python does
1671  PyObjectGuard traceback_module(PyImport_ImportModule("traceback"));
1672  if(traceback_module.get() != NULL)
1673  {
1674  PyObjectGuard format_exception(
1675  PyObject_GetAttrString(traceback_module.get(), "format_exception"));
1676  if(format_exception.get() != NULL)
1677  {
1678  PyObjectGuard formatted(
1679  PyObject_CallFunctionObjArgs(format_exception.get(),
1680  pType,
1681  pValue ? pValue : Py_None,
1682  pTraceback ? pTraceback : Py_None,
1683  NULL));
1684  if(formatted.get() != NULL)
1685  {
1686  // formatted is a list of strings, join them
1687  PyObjectGuard empty_string(PyUnicode_FromString(""));
1688  PyObjectGuard joined(
1689  PyUnicode_Join(empty_string.get(), formatted.get()));
1690  if(joined.get() != NULL)
1691  {
1692  const char* traceback_cstr = PyUnicode_AsUTF8(joined.get());
1693  if(traceback_cstr)
1694  err_msg = traceback_cstr;
1695  }
1696  }
1697  }
1698  }
1699 
1700  // Fallback to simple message if traceback formatting failed
1701  if(err_msg == "Unknown Python Error" && pValue != NULL)
1702  {
1703  PyObjectGuard pStr(PyObject_Str(pValue));
1704  if(pStr.get() != NULL)
1705  {
1706  const char* error_cstr = PyUnicode_AsUTF8(pStr.get());
1707  if(error_cstr)
1708  err_msg = error_cstr;
1709  }
1710  }
1711  }
1712 
1713  Py_XDECREF(pType);
1714  Py_XDECREF(pValue);
1715  Py_XDECREF(pTraceback);
1716 
1717  // Add label prefix if provided
1718  if(!label.empty())
1719  err_msg = label + ":\n" + err_msg;
1720 
1721  return err_msg;
1722 } //end capturePyErr()
1723 
1724 //==============================================================================
1725 std::string ots::ARTDAQSupervisor::captureStderrAndStdout_(std::string label /* = "" */)
1726 {
1727  if(!stringIO_out_)
1728  return ""; // Not defined
1729  // If a Python error is already pending, do not consume it here
1730  if(PyErr_Occurred())
1731  return "";
1732  if(label.size())
1733  label += ' '; //for nice printing
1734 
1735  std::string outString = "";
1736  PyObjectGuard out(PyObject_CallMethod(stringIO_out_, "getvalue", NULL));
1737 
1738  if(checkPythonError(out.get()))
1739  {
1740  // Error getting output - clear the error and return empty string
1741  capturePyErr("captureStderrAndStdout getvalue");
1742  return "";
1743  }
1744 
1745  const char* text = PyUnicode_AsUTF8(out.get());
1746 
1747  return text ? text : "";
1748 } //end captureStderrAndStdout_()
1749 
1750 void ots::ARTDAQSupervisor::getDAQState_()
1751 {
1752  __SUP_COUTS__(50) << "Getting DAQInterface python lock" << __E__;
1753  std::lock_guard<std::recursive_mutex> lk(daqinterface_pythonMutex_);
1754  __SUP_COUTS__(50) << "Have DAQInterface python lock" << __E__;
1755 
1756  if(daqinterface_ptr_ == NULL)
1757  {
1758  daqinterface_state_ = "";
1759  __SUP_COUT_WARN__ << "daqinterface_ptr_ is not initialized!" << __E__;
1760  return;
1761  }
1762 
1763  // Prepare Python Strings ONCE (Move outside loop to prevent 5x memory leak)
1764  PyObjectGuard pName(PyUnicode_FromString("state"));
1765  PyObjectGuard pArg(PyUnicode_FromString("DAQInterface"));
1766 
1767  // WARNING: Verify your Python 'state' method accepts an argument.
1768  // If 'def state(self):' is the signature, passing pArg will fail.
1769  // If so, call: PyObject_CallMethodObjArgs(daqinterface_ptr_, pName, NULL);
1770 
1771  int tries = 0;
1772  while(tries < 5)
1773  {
1774  // Call the method
1775  PyObjectGuard res(
1776  PyObject_CallMethodObjArgs(daqinterface_ptr_, pName.get(), pArg.get(), NULL));
1777 
1778  if(checkPythonError(res.get()))
1779  {
1780  tries++;
1781 
1782  // Get the error message
1783  std::string err_msg = capturePyErr("state");
1784 
1785  std::ostringstream ss;
1786  ss << "Attempt n " << tries
1787  << ". Error calling 'state'. Python Exception: " << err_msg;
1788 
1789  if(tries >= 5)
1790  {
1791  __COUT_ERR__ << ss.str() << __E__; // Log error on final fail
1792  daqinterface_state_ = "ERROR"; // distinct from empty
1793  }
1794  else
1795  {
1796  __COUT__ << ss.str() << __E__; // Log warning
1797  usleep(100000); // 100ms
1798  }
1799  continue;
1800  }
1801 
1802  // --- SUCCESS CASE ---
1803 
1804  // Safely convert result to string (res might not be a string!)
1805  PyObjectGuard strRes(PyObject_Str(res.get())); // Force conversion to string
1806  if(strRes.get())
1807  {
1808  daqinterface_state_ = std::string(PyUnicode_AsUTF8(strRes.get()));
1809  }
1810  else
1811  {
1812  // Rare case: object couldn't be converted to string
1813  daqinterface_state_ = "UNKNOWN";
1814  }
1815 
1816  __SUP_COUTS__(20) << "getDAQState_ state=" << daqinterface_state_ << __E__;
1817  break;
1818  }
1819 
1820  // Cleanup the string objects we created
1821 } //end getDAQState_()
1822 
1823 //==============================================================================
1824 std::string ots::ARTDAQSupervisor::getProcessInfo_(void)
1825 {
1826  __SUP_COUTS__(50) << "Getting DAQInterface state lock" << __E__;
1827  std::lock_guard<std::recursive_mutex> lk(daqinterface_pythonMutex_);
1828  __SUP_COUTS__(50) << "Have DAQInterface state lock" << __E__;
1829 
1830  if(daqinterface_ptr_ == nullptr)
1831  {
1832  return "";
1833  }
1834 
1835  PyObjectGuard pName(PyUnicode_FromString("artdaq_process_info"));
1836  PyObjectGuard pArg(PyUnicode_FromString("DAQInterface"));
1837  PyObjectGuard pArg2(PyBool_FromLong(true));
1838  PyObjectGuard res(PyObject_CallMethodObjArgs(
1839  daqinterface_ptr_, pName.get(), pArg.get(), pArg2.get(), NULL));
1840 
1841  if(checkPythonError(res.get()))
1842  {
1843  std::string err = capturePyErr("artdaq_process_info");
1844  __SS__ << "Error calling artdaq_process_info function: " << err << __E__;
1845  __SUP_SS_THROW__;
1846  return "";
1847  }
1848  //cache status as latest
1849  std::lock_guard<std::mutex> lock(daqinterface_statusMutex_);
1850  daqinterface_status_ = std::string(PyUnicode_AsUTF8(res.get()));
1851  return daqinterface_status_;
1852 } // end getProcessInfo_()
1853 
1854 std::string ots::ARTDAQSupervisor::artdaqStateToOtsState(std::string state)
1855 {
1856  if(state == "nonexistent" || state == "nonexistant")
1857  return RunControlStateMachine::INITIAL_STATE_NAME;
1858  if(state == "Ready")
1859  return "Configured";
1860  if(state == "Running")
1861  return RunControlStateMachine::RUNNING_STATE_NAME;
1862  if(state == "Paused")
1863  return RunControlStateMachine::PAUSED_STATE_NAME;
1864  if(state == "Stopped")
1865  return RunControlStateMachine::HALTED_STATE_NAME;
1866 
1867  TLOG(TLVL_WARNING) << "Unrecognized state name " << state;
1868  return RunControlStateMachine::FAILED_STATE_NAME;
1869 }
1870 
1871 std::string ots::ARTDAQSupervisor::labelToProcType_(std::string label)
1872 {
1873  if(label_to_proc_type_map_.count(label))
1874  {
1875  return label_to_proc_type_map_[label];
1876  }
1877  return "UNKNOWN";
1878 }
1879 
1880 //==============================================================================
1882 std::list<ots::ARTDAQSupervisor::DAQInterfaceProcessInfo>
1883 ots::ARTDAQSupervisor::getAndParseProcessInfo_()
1884 {
1885  std::list<ots::ARTDAQSupervisor::DAQInterfaceProcessInfo> output;
1886  // full acquire from getProcessInfo_ creates mutex locking up!
1887  // auto info = getProcessInfo_();
1888  std::string info;
1889 
1890  std::unique_lock<std::recursive_mutex> lk(daqinterface_pythonMutex_,
1891  std::try_to_lock);
1892  if(!lk.owns_lock()) //if lock not availabe, just report last status
1893  {
1894  __COUTS__(50) << "Do not have python lock." << __E__;
1895  std::lock_guard<std::mutex> lock(daqinterface_statusMutex_);
1896  info = daqinterface_status_;
1897  }
1898  else //have lock! so retrieve Python Interface status
1899  {
1900  __COUTS__(50) << "Have python lock!" << __E__;
1901  info = getProcessInfo_();
1902  }
1903  __COUTVS__(20, info);
1904 
1905  auto procs = tokenize_(info);
1906 
1907  // 0: Whole string
1908  // 1: Process Label
1909  // 2: Process host
1910  // 3: Process port
1911  // 4: Process subsystem
1912  // 5: Process Rank
1913  // 6: Process state
1914  std::regex re("(.*?) at ([^:]*):(\\d+) \\(subsystem (\\d+), rank (\\d+)\\): (.*)");
1915 
1916  for(auto& proc : procs)
1917  {
1918  std::smatch match;
1919  if(std::regex_match(proc, match, re))
1920  {
1921  DAQInterfaceProcessInfo info;
1922 
1923  info.label = match[1];
1924  info.host = match[2];
1925  info.port = std::stoi(match[3]);
1926  info.subsystem = std::stoi(match[4]);
1927  info.rank = std::stoi(match[5]);
1928  info.state = match[6];
1929 
1930  output.push_back(info);
1931  }
1932  }
1933  return output;
1934 } // end getAndParseProcessInfo_()
1935 
1936 //==============================================================================
1938  std::unique_ptr<artdaq::CommanderInterface>>>
1939 ots::ARTDAQSupervisor::makeCommandersFromProcessInfo()
1940 {
1941  std::list<
1942  std::pair<DAQInterfaceProcessInfo, std::unique_ptr<artdaq::CommanderInterface>>>
1943  output;
1944  auto infos = getAndParseProcessInfo_();
1945 
1946  for(auto& info : infos)
1947  {
1948  artdaq::Commandable cm;
1949  fhicl::ParameterSet ps;
1950 
1951  ps.put<std::string>("commanderPluginType", "xmlrpc");
1952  ps.put<int>("id", info.port);
1953  ps.put<std::string>("server_url", info.host);
1954 
1955  output.emplace_back(std::make_pair<DAQInterfaceProcessInfo,
1956  std::unique_ptr<artdaq::CommanderInterface>>(
1957  std::move(info), artdaq::MakeCommanderPlugin(ps, cm)));
1958  }
1959 
1960  return output;
1961 } // end makeCommandersFromProcessInfo()
1962 
1963 //==============================================================================
1964 // getConfiguredArtdaqHosts
1965 // Returns the de-duplicated set of hostnames of all enabled artdaq processes,
1966 // taken from the active configuration via ARTDAQTableBase::extractARTDAQInfo
1967 // (the same call used by configuringThread()). Unlike makeCommandersFromProcessInfo()
1968 // -- which reads the live DAQInterface status and is empty when DAQInterface is
1969 // not running -- this reflects the configuration's intended deployment. The
1970 // configuration does NOT carry the runtime xmlrpc commander ports, so this is
1971 // used for host discovery only (e.g. to drive 'ots -tt <hosts>').
1972 std::set<std::string> ots::ARTDAQSupervisor::getConfiguredArtdaqHosts(void)
1973 {
1974  std::set<std::string> hosts;
1975  try
1976  {
1977  ConfigurationTree supervisorNode = getSupervisorTableNode();
1978  ARTDAQTableBase::ARTDAQInfo info = ARTDAQTableBase::extractARTDAQInfo(
1979  supervisorNode, false /*getStatusFalseNodes*/, false /*doWriteFHiCL*/);
1980  for(const auto& typeProcs : info.processes)
1981  for(const auto& proc : typeProcs.second)
1982  if(proc.status && !proc.hostname.empty())
1983  hosts.insert(proc.hostname);
1984  }
1985  catch(const std::exception& e)
1986  {
1987  __SUP_COUT_ERR__ << "Failed to extract configured artdaq hosts: " << e.what()
1988  << __E__;
1989  }
1990  catch(...)
1991  {
1992  __SUP_COUT_ERR__
1993  << "Failed to extract configured artdaq hosts (unknown exception)." << __E__;
1994  }
1995  __SUP_COUT__ << "Configured artdaq hosts: " << StringMacros::setToString(hosts)
1996  << __E__;
1997  return hosts;
1998 } // end getConfiguredArtdaqHosts()
1999 
2000 //==============================================================================
2001 std::list<std::string> ots::ARTDAQSupervisor::tokenize_(std::string const& input)
2002 {
2003  size_t pos = 0;
2004  std::list<std::string> output;
2005 
2006  while(pos != std::string::npos && pos < input.size())
2007  {
2008  auto newpos = input.find('\n', pos);
2009  if(newpos != std::string::npos)
2010  {
2011  output.emplace_back(input, pos, newpos - pos);
2012  // TLOG(TLVL_TRACE) << "tokenize_: " << output.back();
2013  pos = newpos + 1;
2014  }
2015  else
2016  {
2017  output.emplace_back(input, pos);
2018  // TLOG(TLVL_TRACE) << "tokenize_: " << output.back();
2019  pos = newpos;
2020  }
2021  }
2022  return output;
2023 } // end tokenize_()
2024 
2025 //==============================================================================
2026 void ots::ARTDAQSupervisor::daqinterfaceRunner_()
2027 try
2028 {
2029  TLOG(TLVL_TRACE) << "Runner thread starting";
2030  runner_running_ = true;
2031  while(runner_running_)
2032  {
2033  if(daqinterface_ptr_ != NULL)
2034  {
2035  std::unique_lock<std::recursive_mutex> lk(daqinterface_pythonMutex_);
2036  getDAQState_();
2037  std::string state_before = daqinterface_state_;
2038 
2039  __SUP_COUTS__(2) << "Runner state_before=" << state_before
2040  << " state now=" << daqinterface_state_
2041  << " ?= running, ready, or booted" << __E__;
2042 
2043  if(daqinterface_state_ == "running" || daqinterface_state_ == "ready" ||
2044  daqinterface_state_ == "booted")
2045  {
2046  try
2047  {
2048  TLOG(TLVL_TRACE) << "Calling DAQInterface::check_proc_heartbeats";
2049  PyObjectGuard pName(PyUnicode_FromString("check_proc_heartbeats"));
2050  PyObjectGuard res(
2051  PyObject_CallMethodObjArgs(daqinterface_ptr_, pName.get(), NULL));
2052  __COUT_MULTI_LBL__(1,
2053  captureStderrAndStdout_("check_proc_heartbeats"),
2054  "check_proc_heartbeats");
2055  TLOG(TLVL_TRACE)
2056  << "Done with DAQInterface::check_proc_heartbeats call";
2057 
2058  if(res.get() == NULL)
2059  {
2060  runner_running_ = false;
2061  std::string err = capturePyErr("check_proc_heartbeats");
2062  __SS__ << "Error calling check_proc_heartbeats function: " << err
2063  << __E__;
2064  __SUP_SS_THROW__;
2065  break;
2066  }
2067  }
2068  catch(cet::exception& ex)
2069  {
2070  runner_running_ = false;
2071  std::string err = capturePyErr("check_proc_heartbeats");
2072  __SS__ << "An cet::exception occurred while calling "
2073  "check_proc_heartbeats function "
2074  << ex.explain_self() << ": " << err << __E__;
2075  __SUP_SS_THROW__;
2076  break;
2077  }
2078  catch(std::exception& ex)
2079  {
2080  runner_running_ = false;
2081  std::string err = capturePyErr("check_proc_heartbeats");
2082  __SS__ << "An std::exception occurred while calling "
2083  "check_proc_heartbeats function: "
2084  << ex.what() << "\n\n"
2085  << err << __E__;
2086  __SUP_SS_THROW__;
2087  break;
2088  }
2089  catch(...)
2090  {
2091  runner_running_ = false;
2092  std::string err = capturePyErr("check_proc_heartbeats");
2093  __SS__ << "An unknown Error occurred while calling "
2094  "check_proc_heartbeats function: "
2095  << err << __E__;
2096  __SUP_SS_THROW__;
2097  break;
2098  }
2099 
2100  lk.unlock();
2101  getDAQState_();
2102  if(daqinterface_state_ != state_before)
2103  {
2104  runner_running_ = false;
2105  lk.unlock();
2106  __SS__ << "DAQInterface state unexpectedly changed from "
2107  << state_before << " to " << daqinterface_state_
2108  << ". Check supervisor log file for more info!" << __E__;
2109  __SUP_SS_THROW__;
2110  break;
2111  }
2112  }
2113  }
2114  else
2115  {
2116  __SUP_COUT__ << "daqinterface_ptr_ is null" << __E__;
2117  break;
2118  }
2119  usleep(1000000);
2120  }
2121  runner_running_ = false;
2122  TLOG(TLVL_TRACE) << "Runner thread complete";
2123 } // end daqinterfaceRunner_()
2124 catch(...)
2125 {
2126  __SS__ << "An error occurred in "
2127  "start_runner_/daqinterfaceRunner_ thread "
2128  << __E__;
2129  try
2130  {
2131  throw;
2132  }
2133  catch(const std::runtime_error& e)
2134  {
2135  ss << "Here is the error: " << e.what() << __E__;
2136  }
2137  catch(...)
2138  {
2139  ss << "Unexpected error!" << __E__;
2140  }
2141  __COUT_ERR__ << ss.str();
2142 
2143  {
2144  std::lock_guard<std::mutex> lock(
2145  thread_mutex_); // lock out for remainder of scope
2146  thread_error_message_ = ss.str();
2147  }
2148 
2149  theStateMachine_.setErrorMessage(ss.str());
2150 
2151  sendAsyncExceptionToGateway( //0 for both pause/stop indicates error
2152  ss.str(),
2153  0 /* isPauseException */,
2154  0 /* isStopException */);
2155 
2156 } // end daqinterfaceRunner_() catch
2157 
2158 //==============================================================================
2159 void ots::ARTDAQSupervisor::stop_runner_()
2160 {
2161  runner_running_ = false;
2162  if(runner_thread_ && runner_thread_->joinable())
2163  {
2164  runner_thread_->join();
2165  runner_thread_.reset(nullptr);
2166  }
2167 } // end stop_runner_()
2168 
2169 //==============================================================================
2170 void ots::ARTDAQSupervisor::start_runner_()
2171 {
2172  stop_runner_();
2173  runner_thread_ =
2174  std::make_unique<std::thread>(&ots::ARTDAQSupervisor::daqinterfaceRunner_, this);
2175 } // end start_runner_()
2176 
2177 //==============================================================================
2178 std::string ARTDAQSupervisor::getServiceDataFilePath() const
2179 {
2180  return StringMacros::getPersistentSystemVariablesFilePath();
2181 } // end getServiceDataFilePath()
2182 
2183 //==============================================================================
2184 void ARTDAQSupervisor::initArtdaqSystemVariables()
2185 {
2186  loadArtdaqSystemVariables();
2187 
2188  auto& ns = StringMacros::systemVariables_[ARTDAQ_SYSVAR_NAMESPACE];
2189  __SUP_COUT__ << "Artdaq system variables initialized: "
2190  << StringMacros::mapToString(ns) << __E__;
2191 } // end initArtdaqSystemVariables()
2192 
2193 //==============================================================================
2194 void ARTDAQSupervisor::loadArtdaqSystemVariables()
2195 {
2197  __SUP_COUT__ << "Loaded artdaq system variables from " << getServiceDataFilePath()
2198  << __E__;
2199  else
2200  __SUP_COUT__ << "No persisted artdaq system variables file found at "
2201  << getServiceDataFilePath() << __E__;
2202 } // end loadArtdaqSystemVariables()
2203 
2204 //==============================================================================
2205 void ARTDAQSupervisor::saveArtdaqSystemVariables()
2206 {
2207  std::string filePath = getServiceDataFilePath();
2208  std::ofstream file(filePath);
2209  if(!file.is_open())
2210  {
2211  __SUP_SS__ << "Failed to open file for writing artdaq system variables: "
2212  << filePath << __E__;
2213  __SUP_SS_THROW__;
2214  }
2215 
2216  for(auto& [key, value] : StringMacros::systemVariables_[ARTDAQ_SYSVAR_NAMESPACE])
2217  file << key << "=" << value << "\n";
2218 
2219  __SUP_COUT__ << "Saved artdaq system variables to " << filePath << __E__;
2220 } // end saveArtdaqSystemVariables()
2221 
2222 //==============================================================================
2223 void ARTDAQSupervisor::forceSupervisorPropertyValues(void)
2224 {
2225  CorePropertySupervisorBase::addSupervisorProperty(
2226  CorePropertySupervisorBase::SUPERVISOR_PROPERTIES.AutomatedRequestTypes,
2227  "getSystemVariables | getJsonDocuments");
2228 } // end forceSupervisorPropertyValues()
2229 
2230 //==============================================================================
2231 void ARTDAQSupervisor::request(const std::string& requestType,
2232  cgicc::Cgicc& cgiIn,
2233  HttpXmlDocument& xmlOut,
2234  const WebUsers::RequestUserInfo& /*userInfo*/)
2235 try
2236 {
2237  __SUP_COUT__ << "ARTDAQSupervisor request: " << requestType << __E__;
2238 
2239  if(requestType == "getSystemVariables")
2240  {
2241  for(auto& [key, value] : StringMacros::systemVariables_[ARTDAQ_SYSVAR_NAMESPACE])
2242  xmlOut.addTextElementToData("artdaq_" + key, value);
2243  }
2244  else if(requestType == "setSystemVariable")
2245  {
2246  std::string key = CgiDataUtilities::postData(cgiIn, "key");
2247  std::string value = CgiDataUtilities::postData(cgiIn, "value");
2248 
2249  if(key.empty())
2250  {
2251  xmlOut.addTextElementToData("Error", "Variable key must not be empty.");
2252  return;
2253  }
2254  for(char c : key)
2255  if(!std::isalnum(c) && c != '_')
2256  {
2257  xmlOut.addTextElementToData(
2258  "Error",
2259  "Variable key must contain only alphanumeric characters and "
2260  "underscores.");
2261  return;
2262  }
2263 
2264  StringMacros::systemVariables_[ARTDAQ_SYSVAR_NAMESPACE][key] = value;
2265  saveArtdaqSystemVariables();
2266 
2267  __SUP_COUT__ << "Set artdaq system variable " << key << " = " << value << __E__;
2268  xmlOut.addTextElementToData("Success", "Variable '" + key + "' set.");
2269  }
2270  else if(requestType == "getJsonDocuments")
2271  {
2272  auto* ifc = ConfigurationInterface::getInstance();
2273 
2274  std::set<std::string> allTableNames = ifc->getAllTableNames();
2275 
2276  for(const auto& tableName : allTableNames)
2277  {
2278  if(tableName.find(TableBase::JSON_DOC_PREPEND) != 0)
2279  continue;
2280 
2281  std::string docName = tableName.substr(TableBase::JSON_DOC_PREPEND.size());
2282 
2283  TableBase tmpTable(true, tableName);
2284  std::set<TableVersion> versions = ifc->getVersions(&tmpTable);
2285 
2286  std::string versionList;
2287  for(const auto& v : versions)
2288  {
2289  if(!versionList.empty())
2290  versionList += ",";
2291  versionList += v.toString();
2292  }
2293 
2294  xmlOut.addTextElementToData("jsonDoc_name", docName);
2295  xmlOut.addTextElementToData("jsonDoc_versions", versionList);
2296  }
2297  }
2298  else
2299  {
2300  __SUP_SS__ << "Unknown request type '" << requestType << "' for ARTDAQSupervisor."
2301  << __E__;
2302  __SUP_COUT__ << ss.str();
2303  xmlOut.addTextElementToData("Error", ss.str());
2304  }
2305 }
2306 catch(const std::runtime_error& e)
2307 {
2308  __SUP_SS__ << "Error handling request '" << requestType << "': " << e.what() << __E__;
2309  __SUP_COUT_ERR__ << ss.str();
2310  xmlOut.addTextElementToData("Error", ss.str());
2311 }
2312 catch(...)
2313 {
2314  __SUP_SS__ << "Unknown error handling request '" << requestType << "'." << __E__;
2315  __SUP_COUT_ERR__ << ss.str();
2316  xmlOut.addTextElementToData("Error", ss.str());
2317 } // end request()
void request(const std::string &requestType, cgicc::Cgicc &cgiIn, HttpXmlDocument &xmlOut, const WebUsers::RequestUserInfo &userInfo) override
virtual void transitionHalting(toolbox::Event::Reference event) override
virtual void transitionInitializing(toolbox::Event::Reference event) override
static const std::string ARTDAQ_FCL_PATH
Tree-path rule is, if the last link in the path is a group link with a specified group ID,...
static std::string getBootFileContentFromInfo(const ARTDAQInfo &info, const std::string &setupScript, int debugLevel)
static std::string postData(cgicc::Cgicc &cgi, const std::string &needle)
ConfigurationTree getNode(const std::string &nodeString, bool doNotThrowOnBrokenUIDLinks=false) const
"root/parent/parent/"
ConfigurationTree getNode(const std::string &nodeName, bool doNotThrowOnBrokenUIDLinks=false) const
navigating between nodes
const std::string & getValueAsString(bool returnLinkTableValue=false) const
void getValue(T &value) const
ITRACEController * theTRACEController_
only define for an app that receives a command
bool isComplete()
get functions
Definition: ProgressBar.cc:88
void step()
thread safe
Definition: ProgressBar.cc:74
int read()
if stepsToComplete==0, then define any progress as 50%, thread safe
Definition: ProgressBar.cc:120
void complete()
declare complete, thread safe
Definition: ProgressBar.cc:95
defines used also by OtsConfigurationWizardSupervisor
void INIT_MF(const char *name)
static std::string setToString(const std::set< T > &setToReturn, const std::string &delimeter=", ")
setToString ~
static bool loadPersistentSystemVariables(void)
loads persisted 'artdaq' namespace systemVariables_; returns false if no file found
Definition: StringMacros.cc:49
static std::string mapToString(const std::map< std::string, T > &mapToReturn, const std::string &primaryDelimeter=", ", const std::string &secondaryDelimeter=": ")
std::string name
Also key in map.