diff --git a/larsim/EventGenerator/CMakeLists.txt b/larsim/EventGenerator/CMakeLists.txt index 3636da451..9aec179a6 100644 --- a/larsim/EventGenerator/CMakeLists.txt +++ b/larsim/EventGenerator/CMakeLists.txt @@ -158,6 +158,7 @@ cet_build_plugin(TextFileGen art::EDProducer art::Framework_Services_Registry messagefacility::MF_MessageLogger ROOT::Physics + curl ) install_headers() diff --git a/larsim/EventGenerator/TextFileGen_module.cc b/larsim/EventGenerator/TextFileGen_module.cc index f069700d1..ed185b9af 100644 --- a/larsim/EventGenerator/TextFileGen_module.cc +++ b/larsim/EventGenerator/TextFileGen_module.cc @@ -54,9 +54,75 @@ * The units in LArSoft are cm for distances and ns for time. * The use of `TLorentzVector` below does not imply space and time have the same units * (do not use `TLorentzVector::Boost()`). + * + *=========================================================================== + * + * File and server input + * --------------------- + * + * Orginally TextFileGen was written to read events from a hepevt format text file. + * TextFileGen has now been modified to be able to read hepevt events from a server + * using a specially formatted url. The server should return one hepevt event per + * request. + * + * The choice between file and server input is controlled by fcl parameters + * "InputFileName" and "InputURL." One of these should be spceified, and the + * other left blank or left out of the fcl configuraiton. + * + * This module does not make any assumption about how the url should be constructed, + * except that it will read one hepevt event using using a single http(s) GET request. + * Any parameters that need to be passed to the server should be embedded in the url + * sent to the server via fcl parameter "InputURL." This module does not modify + * the specified url in any way. + * + * If the server is protected behind Fermilab SSO, set fcl parameter UseSSOAuth + * to be true. The remaining fcl parameters (Cert, Key, CertType, KeyType, and + * KeyPasswd) give access to most of the relevant curl options for dealing with + * certificates and keys. When running interactively, these remaining fcl parameters + * can be left as their default values if a certificate is obtained using command "kx509." + * To directly use a p12 certficate obtained from cilogon.org, set the name of the + * certificate file using fcl parameter "Cert" or by setting an environment variable, + * set "CertType" to "P12," and set "KeyPasswd" to the password you entered when you + * obtained the certificate from cilogon.org. Note that grid proxies obtained using + * "voms_proxy_init," including managed proxies, and proxies supplied to batch jobs, + * do not (currently) work for accessing SSO-protected web pages. + * + * Http request statuses 5xx are treated as retriable indefinitely, up to some + * maximum cumulative timeout specified by fcl parameter "Timeout." Other http + * error statuses are treated as fatal errors, and will result in an exception being + * thrown. + * + * The details of how to set up a server, and how to construct a server url, are + * beyond the scope of this comment. However, here are some hints. An example + * cgi server script can be found in larsim/scripts. This script was installed on + * the MicroBooNE web server for testing. Here is a typical server url using this + * installation. + * + * https://microboone-exp.fnal.gov/cgi-bin/hepevt.py?file=HEPevents.txt + * + * H. Greenlee, 17-May-2022 + * + *=========================================================================== + * + * FCL parameters. + * + * InputFileName - Name of hepevt input file (no default). + * Offset - Number of events to skip (for file input, default 0). + * InputURL - Server url (no default). + * Timeout - Maximum server cumulative timeout (seconds, default 7200, 0=none). + * MoveY - Propagate particles to y-plane (default don't propagate). + * UseSSOAuth - Use SSO Auth (certificat/proxy based, default false). + * Cert - Certificate file (defaults: $X509_USER_CERT, $X509_USER_PROXY, /tmp/x509up_u + * Key - Private key file (defaults: $X509_USER_KEY, $X509_USER_PROXY, /tmp/x509up_u + * CertType - Type of certificate file (default: libcurl decides). + * KeyType - Type of key file (default: libcurl decides). + * KeyPasswd - Key file password (default: none). + * + *=========================================================================== */ #include #include +#include #include #include @@ -75,6 +141,8 @@ #include "larcoreobj/SummaryData/RunData.h" #include "nusimdata/SimulationBase/MCTruth.h" #include "nusimdata/SimulationBase/MCParticle.h" +#include +#include // sleep namespace evgen { class TextFileGen; @@ -87,29 +155,99 @@ class evgen::TextFileGen : public art::EDProducer { void produce(art::Event & e) override; void beginJob() override; void beginRun(art::Run & run) override; + void endJob() override; private: + static size_t curl_callback(char* p, size_t size, size_t nmemb, void* userdata); std::pair readEventInfo(std::istream& is); - simb::MCTruth readNextHepEvt(); - unsigned long int fOffset; - std::ifstream* fInputFile; + simb::MCTruth readNextHepEvt(std::istream* is); + unsigned long int fOffset; ///< Number of events to skip from input file. + std::ifstream* fInputFile; ///< Input file stream. std::string fInputFileName; ///< Name of text file containing events to simulate + std::string fInputURL; ///< Input server url. + double fTimeout; ///< Maximum server cumulative timeout. + std::string fCookieFile; ///< Cookie file. double fMoveY; ///< Project particles to a new y plane. + bool fUseSSOAuth; ///< SSO flag. + std::string fCert; ///< Certificate file name. + std::string fKey; ///< Private file name. + std::string fCertType; ///< Certificate file type. + std::string fKeyType; ///< Key file type. + std::string fKeyPasswd; ///< Key file password. }; //------------------------------------------------------------------------------ evgen::TextFileGen::TextFileGen(fhicl::ParameterSet const & p) : EDProducer{p} - , fOffset{p.get("Offset")} + , fOffset{p.get("Offset", 0)} , fInputFile(0) - , fInputFileName{p.get("InputFileName")} + , fInputFileName{p.get("InputFileName", std::string())} + , fInputURL{p.get("InputURL", std::string())} + , fTimeout{p.get("Timeout", 7200.)} , fMoveY{p.get("MoveY", -1e9)} + , fUseSSOAuth{p.get("UseSSOAuth", false)} + , fCert{p.get("Cert", std::string())} + , fKey{p.get("Key", std::string())} + , fCertType{p.get("CertType", std::string())} + , fKeyType{p.get("KeyType", std::string())} + , fKeyPasswd{p.get("KeyPasswd", std::string())} { if (fMoveY>-1e8){ mf::LogWarning("TextFileGen")<<"Particles will be moved to a new plane y = "< >(); produces< sumdata::RunData, art::InRun >(); @@ -119,24 +257,42 @@ evgen::TextFileGen::TextFileGen(fhicl::ParameterSet const & p) //------------------------------------------------------------------------------ void evgen::TextFileGen::beginJob() { - fInputFile = new std::ifstream(fInputFileName.c_str()); + if(!fInputFileName.empty()) { + fInputFile = new std::ifstream(fInputFileName.c_str()); - // check that the file is a good one - if( !fInputFile->good() ) - throw cet::exception("TextFileGen") << "input text file " - << fInputFileName - << " cannot be read.\n"; + // check that the file is a good one + if( !fInputFile->good() ) + throw cet::exception("TextFileGen") << "input text file " + << fInputFileName + << " cannot be read.\n"; - for (unsigned i = 0; i != fOffset; ++i) { - auto const [eventNo, nparticles] = readEventInfo(*fInputFile); - for (unsigned p = 0; p != nparticles; ++p) { - constexpr auto all_chars_until = std::numeric_limits::max(); - fInputFile->ignore(all_chars_until, '\n'); + for (unsigned i = 0; i != fOffset; ++i) { + auto const [eventNo, nparticles] = readEventInfo(*fInputFile); + for (unsigned p = 0; p != nparticles; ++p) { + constexpr auto all_chars_until = std::numeric_limits::max(); + fInputFile->ignore(all_chars_until, '\n'); + } } } + // Maybe make cookie file. + + if(fUseSSOAuth) { + char name[24]; + strcpy(name, "/tmp/textfilegen.XXXXXX"); + mkstemp(name); + fCookieFile = std::string(name); + } +} + +//------------------------------------------------------------------------------ +void evgen::TextFileGen::endJob() +{ + // Maybe delete cookie file. + if(fUseSSOAuth) + unlink(fCookieFile.c_str()); } //------------------------------------------------------------------------------ @@ -149,27 +305,253 @@ void evgen::TextFileGen::beginRun(art::Run& run) //------------------------------------------------------------------------------ void evgen::TextFileGen::produce(art::Event & e) { - // check that the file is still good - if( !fInputFile->good() ) - throw cet::exception("TextFileGen") << "input text file " - << fInputFileName - << " cannot be read in produce().\n"; + //Now, read the Event to be used. + auto truthcol = std::make_unique>(); + if(!fInputFileName.empty()) { + + // Input from file. + // Check that the file is still good + + if( !fInputFile->good() ) + throw cet::exception("TextFileGen") << "input text file " + << fInputFileName + << " cannot be read in produce().\n"; + truthcol->push_back(readNextHepEvt(fInputFile)); + } + else if(!fInputURL.empty()) { + // Input from server. -//Now, read the Event to be used. + int delay = 0; + double total_delay = 0.; + std::stringstream ss; + // Make curl handle and set global options. + char errorbuf[CURL_ERROR_SIZE]; + errorbuf[0] = 0; + CURL* c = curl_easy_init(); + curl_easy_setopt(c, CURLOPT_CAPATH, "/cvmfs/oasis.opensciencegrid.org/mis/certificates"); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1); + curl_easy_setopt(c, CURLOPT_WRITEDATA, &ss); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, curl_callback); + curl_easy_setopt(c, CURLOPT_AUTOREFERER, 1); + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errorbuf); + + // Retry loop. + + for(;;) { + + // Increasing delay period starts on second execution of retry loop. + + if(delay > 0) { + if(total_delay > fTimeout && fTimeout > 0.) + throw cet::exception("TextFileGen") << "Exceeded maximum server cumulative timeout.\n"; + mf::LogInfo("TextFileGen") << "Server unavailable, wait " << delay << " seconds."; + sleep(delay); + total_delay += delay; + delay *= 2; + if(delay > 120) + delay = 120; + } + else + delay = 10; -// check that the file is still good - if( !fInputFile->good() ) - throw cet::exception("TextFileGen") << "input text file " - << fInputFileName - << " cannot be read in produce().\n"; - auto truthcol = std::make_unique>(); - truthcol->push_back(readNextHepEvt()); + // Set url. + curl_easy_setopt(c, CURLOPT_URL, fInputURL.c_str()); + curl_easy_setopt(c, CURLOPT_HTTPGET, 1); + + // Set SSO options (if requested.) + + if(fUseSSOAuth) { + + // Set cert and key options. + + curl_easy_setopt(c, CURLOPT_SSLCERT, fCert.c_str()); + curl_easy_setopt(c, CURLOPT_SSLKEY, fKey.c_str()); + if(!fCertType.empty()) + curl_easy_setopt(c, CURLOPT_SSLCERTTYPE, fCertType.c_str()); + if(!fKeyType.empty()) + curl_easy_setopt(c, CURLOPT_SSLKEYTYPE, fKeyType.c_str()); + if(!fKeyPasswd.empty()) + curl_easy_setopt(c, CURLOPT_SSLKEYPASSWD, fKeyPasswd.c_str()); + + // Cookies. + // The cookie generated on the first call in a job will allow to + // bypass the follow-up request in subsequent calls in the same job. + + curl_easy_setopt(c, CURLOPT_COOKIEFILE, fCookieFile.c_str()); + curl_easy_setopt(c, CURLOPT_COOKIEJAR, fCookieFile.c_str()); + curl_easy_setopt(c, CURLOPT_COOKIE, "pfidpaid=ad..CILogonForm"); + } + + // Read data. + + CURLcode res = curl_easy_perform(c); + if(res != CURLE_OK) { + std::ostringstream ss; + ss << "Curl returned status " << res << " from url " << fInputURL << "\n" + << curl_easy_strerror(res) << "\n" + << errorbuf; + mf::LogError("TextFileGen") << ss.str(); + throw cet::exception("TextFileGen") << ss.str() << "\n"; + } + + // Get http response code. + // Common codes: + // 200 - success. + // 404 - Not found. + // 503 - Service unavailable (temporarily). + + long http_response = 0; + curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, &http_response); + //std::cout << "http response = " << http_response << std::endl; + + // Any response 5xx, retry. + + if(http_response >= 500 && http_response < 600) { + ss = std::stringstream(); + continue; + } + + // At this point, if any repsonse except 200, throw exception. + + if(http_response != 200) { + throw cet::exception("TextFileGen") << "Got http response code = " << http_response << " reading url " << fInputURL << "\n"; + } + + // Check content type. + + char* ct; + curl_easy_getinfo(c, CURLINFO_CONTENT_TYPE, &ct); + std::string cts(ct); + cts.erase(cts.find(";")); + //std::cout << "Content type: " << cts << std::endl; + + // If content type is text/plain, assume content is hepevt event. + // We are done. + + if(cts == std::string("text/plain")) + break; + + // If we requested SSO authentication, and we got back an html document, + // assume this is an SSO form, and we need a follow up request. + // Anything else is an error (throw exception). + + if(!fUseSSOAuth || cts != std::string("text/html")) + throw cet::exception("TextFileGen") << "Unknown content type " << cts << "\n"; + + // Extract action url. + + std::string doc = ss.str(); + size_t n = doc.find("action=") + 8; + if(n == std::string::npos) + throw cet::exception("TextFileGen") << "No action using SSO.\n"; + int m = doc.find("\"", n); + std::string action = doc.substr(n, m-n); + //std::cout << "action = " << action << std::endl; + + // Extract SAMLResponse. + + n = doc.find("name=\"SAMLResponse\""); + if(n == std::string::npos) + throw cet::exception("TextFileGen") << "No SAMLResponse using SSO.\n"; + n = doc.find("value=", n) + 7; + m = doc.find("\"", n); + std::string saml = doc.substr(n, m-n); + char* cs = curl_easy_escape(c, saml.c_str(), 0); + std::string saml_urlencode = std::string(cs); + curl_free(cs); + //std::cout << "SAMLResponse = " << saml << std::endl; + + // Extract RelayState. + + n = doc.find("name=\"RelayState\""); + if(n == std::string::npos) + throw cet::exception("TextFileGen") << "No RelayState using SSO.\n"; + n = doc.find("value=", n) + 7; + m = doc.find("\"", n); + std::string relay = doc.substr(n, m-n); + cs = curl_easy_escape(c, relay.c_str(), 0); + std::string relay_urlencode = std::string(cs); + curl_free(cs); + //std::cout << "RelayState= = " << relay << std::endl; + + // Prepare to issue follow up request. + + ss = std::stringstream(); + curl_easy_setopt(c, CURLOPT_URL, action.c_str()); + + // Construct post data for follow up request. + + std::stringstream postargs; + postargs << "RelayState=" << relay_urlencode << "&SAMLResponse=" << saml_urlencode; + //std::cout << "postargs = " << postargs.str() << std::endl; + curl_easy_setopt(c, CURLOPT_POST, 1); + std::string s = postargs.str(); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, s.c_str()); + + // Do request. + + res = curl_easy_perform(c); + if(res != CURLE_OK) { + std::ostringstream ss; + ss << "Curl returned status " << res << " doing follow up request from url " << fInputURL << "\n" + << curl_easy_strerror(res) << "\n" + << errorbuf; + mf::LogError("TextFileGen") << ss.str(); + throw cet::exception("TextFileGen") << ss.str() << "\n"; + } + + // Get http response code. + + http_response = 0; + curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, &http_response); + + // Any response 5xx, retry. + + if(http_response >= 500 && http_response < 600) { + ss = std::stringstream(); + continue; + } + + // Any response except 200, throw exception. + + if(http_response != 200) { + throw cet::exception("TextFileGen") << "Got http response code = " << http_response << " reading url " << fInputURL << "\n"; + } + + // Check content type. + + curl_easy_getinfo(c, CURLINFO_CONTENT_TYPE, &ct); + cts = std::string(ct); + cts.erase(cts.find(";")); + //std::cout << "Content type: " << cts << std::endl; + + // Any content type except "text/plain," throw exception. + + if(cts != std::string("text/plain")) + throw cet::exception("TextFileGen") << "Bad content type " << cts << "\n"; + + // Done (success). + + break; + + } // end of retry loop + + // Cleanup curl handle. + + curl_easy_cleanup(c); + + // Process event data. + // If we exited the retry loop without throwing an exception, hepevt data is + // contained in stringstream ss. + + truthcol->push_back(readNextHepEvt(&ss)); + } e.put(std::move(truthcol)); } @@ -177,7 +559,7 @@ void evgen::TextFileGen::produce(art::Event & e) -simb::MCTruth evgen::TextFileGen::readNextHepEvt() +simb::MCTruth evgen::TextFileGen::readNextHepEvt(std::istream* is) { // declare the variables for reading in the event record @@ -203,7 +585,7 @@ simb::MCTruth evgen::TextFileGen::readNextHepEvt() std::string oneLine; std::istringstream inputLine; simb::MCTruth nextEvent; - auto const [eventNo, nParticles] = readEventInfo(*fInputFile); + auto const [eventNo, nParticles] = readEventInfo(*is); @@ -211,7 +593,8 @@ simb::MCTruth evgen::TextFileGen::readNextHepEvt() // in this interaction. only particles with // status = 1 get tracked in Geant4. for(unsigned short i = 0; i < nParticles; ++i){ - std::getline(*fInputFile, oneLine); + std::getline(*is, oneLine); + //std::cout << oneLine << std::endl; inputLine.clear(); inputLine.str(oneLine); @@ -255,6 +638,7 @@ std::pair evgen::TextFileGen::readEventInfo(std::istream& is { std::string line; getline(iss, line); + //std::cout << line << std::endl; std::istringstream buffer{line}; // Parse read line for the event number and particles per event @@ -264,6 +648,17 @@ std::pair evgen::TextFileGen::readEventInfo(std::istream& is } +size_t evgen::TextFileGen::curl_callback(char* p, size_t size, size_t nmemb, void* userdata) +{ + // Add data to stringstram pointed to by userdata. + + std::stringstream* ss = reinterpret_cast(userdata); + ss->write(p, nmemb); + + // Done. + + return nmemb; +} diff --git a/scripts/hepevt.py b/scripts/hepevt.py new file mode 100755 index 000000000..0fe32017b --- /dev/null +++ b/scripts/hepevt.py @@ -0,0 +1,629 @@ +#! /bin/env python +#================================================================================= +# +# Name: hepevt.py +# +# Purpose: CGI script for serving HepEvt or HepMC events. +# +# Created: 11-May-2022 H. Greenlee +# +# Usage: +# +# hepevt.py +# +# CLI arguments (for testing): +# +# -h|--help - Print help message. +# -f|--file - HepEvt or HepMC file. +# --format - File format, "hepevt" or "hepmc" (default "hepevt"). +# -s|--stream - Event counter stream name (default='default'). +# -u|--user - User name (default none or SSO userid). +# -e|--event - Specify event number. +# -r|--reset - Reset event counter for the specified stream. +# --sleep - Sleep time per event (default 0) +# --min_event - Minimum event number. +# --max_event - Maximum event number. +# +# CGI arguments: +# +# file - Name of hepevt or hepmc file. +# format - File format, "hepevt" or "hepmc" (default "hepevt"). +# stream - Event counter stream name (default='default'). +# user - User name (default none or SSO userid). +# event - Specify event number. +# reset - Event counter reset flag (reset if "1"). +# sleep - Sleep time per event. +# min_event - Minimum event number. +# max_event - Maximum event number. +# +# Usage notes. +# +# 1. This script is python 2/3 agnostic. When invoked on the web server, it uses +# the system python (current python 2.7 on the MicroBooNE web server). It also +# works using larsoft environment pythons, whether python 2 or 3. +# +# 2. This script accepts command line arguments or CGI arguments embedded in a url. +# +# 3. The HepEvt or HepMC file name should be specified as a plain file or relative +# path name (absolute path names are not allowed for security reasons). If a +# file specified using a relative path is not found relative to the current +# directory, this script searches for the file using a search path that consists +# of the following directories. +# +# a) $DOCUMENT_ROOT/../data/hepevt +# b) /web/sites//-exp.fnal.gov/data/hepevt (linux group name) +# c) /web/sites//${GROUP}-exp.fnal.gov/data/hepevt (group env variable) +# +# 4. HepMC format can be auto-sensed, making the format argument optional for either +# HepEvt or HepMC format files. +# +# 5. The event counter file is located in the same directory as the HepEvt file. +# This means that this script requires write access to this directory. +# +# 6. The event counter file contains the event number and byte offset of the most +# recently read HepEvt event. If no events have been read, the event counter file +# doesn't exist. +# +# 7. The name of event counter file is as follows: +# ...next +# The user name will be included if specified by argument "user", or if known +# via SSO authentication. The stream name is as specified by argument "stream". +# +# 8. If a minimum or maximum event number is specified, returned events are limited +# to the range min_event <= e < max_event. +# +# 9. This script uses posix file locking to prevent multiple processes from +# updating the event counter at the same time. If the script can't acquire a +# lock, this script reutrns error 503 (service unavailable) so as to not block +# the entire server. In such cases, the client should retry the operation. +# Resetting the event counter does not make use of file locking. +# +# 10. The sleep time per event option can be used to reduce rate at which events +# can be delivered. It is intended for testing, and is not useful for production. +# +# 11. If a specific event number is specified, the event counter file is not +# read or updated. +# +# 12. This script may return the following error codes. +# 404 - Not found. +# This error is returned for various errors, including: +# a) No HepEvent file was specified. +# b) Specified HepEvent file does not exist. +# c) Invalud event number or attempt to read past end of file. +# 503 - Service unavailable. +# Couldn't acquire exclusive lock on event counter. Client should retry. +# +# +#================================================================================= + +from __future__ import print_function +import sys, os, fcntl, time, grp +import cgi +import cgitb +cgitb.enable() + + +# Global variables + +file_format = '' + + +# Print help. + +def help(): + + filename = sys.argv[0] + file = open(filename) + + doprint=0 + + for line in file.readlines(): + if line.startswith('# hepevt.py'): + doprint = 1 + elif line.startswith('# Usage notes'): + doprint = 0 + if doprint: + if len(line) > 2: + print(line[2:], end='') + else: + print() + + +# Seek from the current file position to the specified, or greater, event number. +# +# Returns the actual event number, or -1 if no matching event is found. + +def seek_event_number(fhepevt, evnum): + + global file_format + + result = -1 + + # Read lines until we find a compatible event number. + + while True: + pos = fhepevt.tell() + line = fhepevt.readline() + if line == '': + result = -1 + break + words = line.split() + + # Check event number. + + if file_format == 'hepevt': + + # Hepevt format. + # New events are indicated by a line with two words: + # + + if len(words) == 2 and words[0].isdigit() and words[1].isdigit(): + n = int(words[0]) + if n >= evnum: + fhepevt.seek(pos) + result = n + break + + # If this looks like a hepmc event header, switch the format. + + elif len(words) == 4 and words[0] == 'E' and words[1].isdigit() and \ + words[2].isdigit() and words[3].isdigit(): + file_format = 'hepmc' + fhepevt.seek(pos) + continue + + + elif file_format == 'hepmc': + + # Hepmc format. + # New events are indicated by a line with four words: + # E + + if len(words) == 4 and words[0] == 'E' and words[1].isdigit() and \ + words[2].isdigit() and words[3].isdigit(): + n = int(words[1]) + if n >= evnum: + fhepevt.seek(pos) + result = n + break + + else: + + # Unknown format. + + break + + + # Done. + + return result + + +# Function to seek to the next event, as specified in the event counter file. +# +# If successful, this function will increment and update the event counter. +# +# Returns an error code, which can be any of the following. +# +# 0 - Success. +# 404 - Couldn't find event. +# 503 - Couldn't obtain an exclusive lock on event counter file. + +def seek_next_event(fhepevt, event_counter_path, min_event, max_event, sleep_time): + + # Open the event counter file for updating. + # This open mode will create the file if it doesn't exist, but won't truncate it if + # it does exist. + + flock = open(event_counter_path, 'a+') + flock.seek(0, 2) # Seek to end of file (necessary in python 2.7) + + # Attempt to obtain an exclusive lock. + # Return error if unsuccessful. + + lockok = False + try: + fcntl.lockf(flock, fcntl.LOCK_EX | fcntl.LOCK_NB) # Exclusive non-blocking. + lockok = True + except: + lockok = False + if not lockok: + flock.close() + return 503 + + # Read existing event number, if any. + # Seek to last read event position. + + evnum = -1 + if flock.tell() > 0: + flock.seek(0) + line = flock.readline() + words = line.split() + evnum = int(words[0]) + pos = int(words[1]) + fhepevt.seek(pos) + else: + evnum = -1 + + # Seek to the next event number. + + seek_event = evnum + 1 + if seek_event < min_event: + seek_event = min_event + new_evnum = seek_event_number(fhepevt, seek_event) + if new_evnum < 0 or (max_event > 0 and new_evnum >= max_event): + flock.close() + return 400 + + # Store the newly found event number and position back in the event counter file. + + flock.seek(0) + flock.truncate() + flock.write('%d %d\n' % (new_evnum, fhepevt.tell())) + + # Close event counter file. This will also release the lock. + + if sleep_time > 0.: + time.sleep(sleep_time) + flock.close() + + # Done + + return 0 + + +# Read and print out one event from file. + +def read_event(f): + + global file_format + + if file_format == 'hepevt': + + # Hepevt format. + # Read and print event header line. + + line = f.readline() + print(line, end='') + words = line.split() + npart = int(words[1]) + + # Read and print particles. + + while npart > 0: + line = f.readline() + print(line, end='') + npart -= 1 + + elif file_format == 'hepmc': + + # Hepmc format. + # Read and print event header line. + + line = f.readline() + print(line, end='') + + # Read and print remaining lines until we get to the next event or footer or end-of-file. + + while True: + pos = f.tell() + line = f.readline() + if line == '': + break + words = line.split() + if words[0] == 'E': + f.seek(pos) + break + if line.startswith('HepMC::'): + break + print(line, end='') + + # Done. + + return + + +# Construct HepEvt search path. + +def search_path(): + + result = [] + + # Based on $DOCUMENT_ROOT + # This is the main method on the web server. + + if 'DOCUMENT_ROOT' in os.environ: + ddir = os.environ['DOCUMENT_ROOT'] + pdir = os.path.dirname(ddir) + dir = os.path.join(pdir, 'data/hepevt') + if os.path.isdir(dir) and not dir in result: + result.append(dir) + + # Based on $EXPERIMENT. + + if 'EXPERIMENT' in os.environ: + exp = os.environ['EXPERIMENT'] + dir = '/web/sites/%s/%s-exp.fnal.gov/data/hepevt' % (exp[0], exp) + if os.path.isdir(dir) and not dir in result: + result.append(dir) + + # Based on linux group name. + + exp = grp.getgrgid(os.getgid())[0] + dir = '/web/sites/%s/%s-exp.fnal.gov/data/hepevt' % (exp[0], exp) + if os.path.isdir(dir) and not dir in result: + result.append(dir) + + # Done + + return result + + +# Main procedure. + +def main(argv): + + global file_format + + # Extract arguments. + + hepevt_file_name = '' + file_format = 'hepevt' + stream_name = 'default' + user = '' + if 'SSO_USERID' in os.environ: + user = os.environ['SSO_USERID'] + evnum = None + reset = None + sleep_time = 0 + min_event = 0 + max_event = 0 + + # Parse command line arguments. + + args = argv[1:] + while len(args) > 0: + if args[0] == '-h' or args[0] == '--help': + + # Help. + + help() + return 0 + + elif args[0] == '-r' or args[0] == '--reset': + + # Reset event counter flag. + + reset = '1' + del args[0] + + elif len(args) > 1 and (args[0] == '-f' or args[0] == '--file'): + + # HepEvt file. + + hepevt_file_name = args[1] + del args[0:2] + + elif len(args) > 1 and args[0] == '--format': + + # File format, hepevt or hepmc. + + file_format = args[1] + del args[0:2] + + elif len(args) > 1 and (args[0] == '-s' or args[0] == '--stream'): + + # Stream name + + stream_name = args[1] + del args[0:2] + + elif len(args) > 1 and (args[0] == '-u' or args[0] == '--user'): + + # User name + + user = args[1] + del args[0:2] + + elif len(args) > 1 and (args[0] == '-e' or args[0] == '--event'): + + # Event number + + evnum = int(args[1]) + del args[0:2] + + elif len(args) > 1 and args[0] == '--sleep': + + # Sleep time per event + + sleep_time = float(args[1]) + del args[0:2] + + elif len(args) > 1 and args[0] == '--min_event': + + # Minimum event number + + min_event = int(args[1]) + del args[0:2] + + elif len(args) > 1 and args[0] == '--max_event': + + # Maximum event number + + max_event = int(args[1]) + del args[0:2] + + elif args[0][0] == '-': + + # Unknown option. + + print('Unknown option %s' % args[0]) + return 1 + + else: + + # Arguments. + + print('Unknown argument %s' % args[0]) + return 1 + + # Parse CGI arguments. + + args = cgi.FieldStorage() + if 'file' in args: + hepevt_file_name = args['file'].value + if 'format' in args: + file_format = args['format'].value + if 'stream' in args: + stream_name = args['stream'].value + if 'user' in args: + user = args['user'].value + if 'event' in args: + evnum = int(args['event'].value) + if 'reset' in args: + reset = args['reset'].value + if 'sleep' in args: + sleep_time = float(args['sleep'].value) + if 'min_event' in args: + min_event = int(args['min_event'].value) + if 'max_event' in args: + max_event = int(args['max_event'].value) + + # It is an error of HepEvt file is not specified. + + if hepevt_file_name == '': + print('Content-type: text/plain') + print('Status: 404 Not Found') + print('') + print('No HepEvt file specified.') + return 0 + + # Reject absolute file paths. + + if hepevt_file_name[0] == '/' or hepevt_file_name[0] == '.': + print('Content-type: text/plain') + print('Status: 404 Not Found') + print('') + print('Absolute path not allowed.') + return 0 + + # Locate HepEvt file. + # Return error 404 if not found. + + hepevt_file_path = hepevt_file_name + if not os.path.exists(hepevt_file_path): + for dir in search_path(): + hepevt_file_path = os.path.join(dir, hepevt_file_name) + if os.path.exists(hepevt_file_path): + break + if not os.path.exists(hepevt_file_path): + print('Content-type: text/plain') + print('Status: 404 Not Found') + print('') + print('HepEvt file %s does not exist.' % hepevt_file_name) + return 0 + + # Check file format. + + if file_format != 'hepevt' and file_format != 'hepmc': + print('Content-type: text/plain') + print('Status: 404 Not Found') + print('') + print('Invalid file format %s.' % file_format) + return 0 + + # Construct path of the event counter file. + # At this point, this file may or may not exist. + + event_counter_path = '' + if user != '': + event_counter_path = '%s.%s.%s.next' % (hepevt_file_path, user, stream_name) + else: + event_counter_path = '%s.%s.next' % (hepevt_file_path, stream_name) + + # Reset event counter? + + if reset == '1': + if evnum == None: + if os.path.exists(event_counter_path): + os.remove(event_counter_path) + else: + f = open(event_counter_path, 'w') + f.write('%d 0\n' % evnum) + f.close() + print('Content-type: text/plain') + print('Status: 200 OK') + print('') + if evnum == None: + print('Reset event counter.') + else: + print('Reset event counter to %d.' % evnum) + return 0 + + # Open HepEvt file. + + fhepevt = open(hepevt_file_path) + + # Seek to the desired event, either by event number or event counter. + + status = 0 + if evnum == None: + + # Seek to next event. + + status = seek_next_event(fhepevt, event_counter_path, min_event, max_event, sleep_time) + + else: + + # Seek to event number + + new_evnum = seek_event_number(fhepevt, evnum) + if new_evnum < 0: + status = 400 + + # Check for errors. + + if status == 400: + print('Content-type: text/plain') + print('Status: 400 Seek Error') + print('') + print('Event not found.') + return 0 + elif status == 404: + print('Content-type: text/plain') + print('Status: 404 Not Found') + print('') + print('Event not found.') + return 0 + elif status == 503: + print('Content-type: text/plain') + print('Status: 503 Service unavailable') + print('') + print('Access to event counter temporarily unavailable.') + return 0 + elif status != 0: + print('Content-type: text/plain') + print('Status: %d' % status) + print('') + return 0 + + # Don't expect errors from here on. + # Print header. + + print('Content-type: text/plain') + print('Status: 200 OK') + print('') + + # Print selected event. + + read_event(fhepevt) + + # Done. + + return 0 + + +# Invoke main program. + +if __name__ == "__main__": + sys.exit(main(sys.argv))