diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1d1bb1e..146527f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,6 +6,12 @@ repos: args: [--style=google] - id: cpplint args: [--filter=-build/include_order] + exclude: | + (?x)^( + subprojects/.*/include/.*\.skel\.h| + subprojects/.*/src/vmlinux\.h| + include/third-party/.* + )$ - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.1.0 hooks: diff --git a/examples/csv-testing.cpp b/examples/csv-testing.cpp index eb1cfcd..e6b64f1 100644 --- a/examples/csv-testing.cpp +++ b/examples/csv-testing.cpp @@ -6,12 +6,12 @@ * @copyright Copyright (c) 2024. See License for Licensing */ +#include +#include #include #include #include -#include - using namespace efimon; // NOLINT int main(int /*argc*/, char** /*argv*/) { diff --git a/examples/frequency-query.cpp b/examples/frequency-query.cpp index 7286d9c..dafb940 100644 --- a/examples/frequency-query.cpp +++ b/examples/frequency-query.cpp @@ -6,12 +6,12 @@ * @copyright Copyright (c) 2024. See License for Licensing */ -#include - #include +#include #include #include +#include int main(int, char **) { efimon::CPUInfo info{}; diff --git a/examples/meson.build b/examples/meson.build index 749a836..102025e 100644 --- a/examples/meson.build +++ b/examples/meson.build @@ -160,3 +160,15 @@ if enable_ptrace_capstone install : false, ) endif + +if enable_sampling_by_pid + executable('sampling-by-pid-testing', + [ + files('sampling-by-pid-testing.cpp') + ], + cpp_args : cpp_args, + include_directories : [project_inc], + dependencies: [project_deps, libefimon_dep], + install : false, + ) +endif diff --git a/examples/process-manager-threaded.cpp b/examples/process-manager-threaded.cpp index e9195c9..cf4e382 100644 --- a/examples/process-manager-threaded.cpp +++ b/examples/process-manager-threaded.cpp @@ -6,12 +6,11 @@ * @copyright Copyright (c) 2024. See License for Licensing */ -#include -#include - #include #include // NOLINT #include // NOLINT +#include +#include #include #include // NOLINT #include diff --git a/examples/process-manager.cpp b/examples/process-manager.cpp index 31f79f9..2634591 100644 --- a/examples/process-manager.cpp +++ b/examples/process-manager.cpp @@ -6,10 +6,9 @@ * @copyright Copyright (c) 2024. See License for Licensing */ +#include #include #include - -#include #include #include #include diff --git a/examples/process-tracking.cpp b/examples/process-tracking.cpp index 1a89bdb..095eec9 100644 --- a/examples/process-tracking.cpp +++ b/examples/process-tracking.cpp @@ -9,13 +9,12 @@ #include #include +#include #include #include #include #include -#include - int main(int argc, char **argv) { std::vector users; diff --git a/examples/process-tree.cpp b/examples/process-tree.cpp index 0c95762..98478e0 100644 --- a/examples/process-tree.cpp +++ b/examples/process-tree.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include diff --git a/examples/sampling-by-pid-testing.cpp b/examples/sampling-by-pid-testing.cpp new file mode 100644 index 0000000..01e86bd --- /dev/null +++ b/examples/sampling-by-pid-testing.cpp @@ -0,0 +1,67 @@ +/** + * @file sampling-by-pid-testing.cpp + * @author Diego Avila (diego.avila@uned.cr) + * @brief Example of eBPF Sampling-by-PID observer testing + * + * @copyright Copyright (c) 2024. See License for Licensing + */ + +#include + +#include +#include +#include + +using namespace efimon; // NOLINT + +int main(int argc, char **argv) { + if (argc <= 1) { + std::cerr << "No PID specified" << std::endl; + return -1; + } + + uint u_pid = std::atoi(argv[1]); + std::cout << "PID: " << u_pid << std::endl; + + SamplingByPIDObserver ob_sampling{u_pid}; + ob_sampling.SetInterval(2000); + + std::cout << "Sampling for 2 seconds..." << std::endl; + auto st_ret = ob_sampling.Trigger(); + if (st_ret.code != Status::OK) { + std::cerr << "ERROR: " << st_ret.msg << std::endl; + return -1; + } + + std::cout << "Raw eBPF samples collected: " + << ob_sampling.GetCollectedSamplesCount() << std::endl; + std::cout << "Decoded userspace samples: " + << ob_sampling.GetDecodedSamplesCount() << std::endl; + + auto *p_readings_ann = + dynamic_cast(ob_sampling.GetReadings()[0]); + + std::cout << "Histogram:" << std::endl; + for (const auto &kv_histogram : p_readings_ann->histogram) { + std::cout << "\t" << std::get<0>(kv_histogram) << ": " + << std::get<1>(kv_histogram) << std::endl; + } + + std::cout << "Classification:" << std::endl; + for (const auto &kv_type : p_readings_ann->classification) { + std::cout << "\t" << AsmClassifier::TypeString(kv_type.first) << ": " + << std::endl; + for (const auto &kv_family : kv_type.second) { + std::cout << "\t\t" << AsmClassifier::FamilyString(kv_family.first) + << ": " << std::endl; + for (const auto &kv_origin : kv_family.second) { + std::cout << "\t\t\t" << AsmClassifier::OriginString(kv_origin.first) + << ": " << kv_origin.second << std::endl; + } + } + } + + st_ret = ob_sampling.Trigger(); + + return 0; +} diff --git a/examples/sqlite-testing.cpp b/examples/sqlite-testing.cpp index 163ad19..45d9f88 100644 --- a/examples/sqlite-testing.cpp +++ b/examples/sqlite-testing.cpp @@ -6,12 +6,12 @@ * @copyright Copyright (c) 2024. See License for Licensing */ +#include +#include #include #include #include -#include - using namespace efimon; // NOLINT int main(int /*argc*/, char** /*argv*/) { diff --git a/include/efimon/asm-classifier.hpp b/include/efimon/asm-classifier.hpp index 014bd76..ee11328 100644 --- a/include/efimon/asm-classifier.hpp +++ b/include/efimon/asm-classifier.hpp @@ -92,8 +92,9 @@ enum class DataOrigin { * The first type determines the instruction type, the second the family or * group and the third the data origin */ -using InstructionPair = - std::tuple; +using InstructionPair = std::tuple; // NOLINT /** * Interface to classify the instructions into families and types @@ -108,9 +109,8 @@ class AsmClassifier { * @param operands operands types * @return InstructionPair */ - virtual InstructionPair Classify(const std::string &inst, - const std::string &operands) const - noexcept = 0; + virtual InstructionPair Classify( + const std::string &inst, const std::string &operands) const noexcept = 0; /** * Determines if the operands belong to memory, immediate or register values @@ -118,8 +118,8 @@ class AsmClassifier { * @param operands as it comes from objdump * @return string with r, i or m symbolising the type of operands */ - virtual const std::string OperandTypes(const std::string &operands) const - noexcept = 0; + virtual const std::string OperandTypes( + const std::string &operands) const noexcept = 0; /** * Default destructor for inheritance (implementation) diff --git a/include/efimon/asm-classifier/x86-classifier.hpp b/include/efimon/asm-classifier/x86-classifier.hpp index 309a01f..236d7fd 100644 --- a/include/efimon/asm-classifier/x86-classifier.hpp +++ b/include/efimon/asm-classifier/x86-classifier.hpp @@ -37,8 +37,8 @@ class x86Classifier : public AsmClassifier { * @param operands as it comes from objdump * @return string with r, i or m symbolising the type of operands */ - const std::string OperandTypes(const std::string &operands) const - noexcept override; + const std::string OperandTypes( + const std::string &operands) const noexcept override; /** * Default destructor for inheritance (implementation) diff --git a/include/efimon/ebpf-modules/cpu-assembly-sampler/meson.build b/include/efimon/ebpf-modules/cpu-assembly-sampler/meson.build new file mode 100644 index 0000000..4d9f8f7 --- /dev/null +++ b/include/efimon/ebpf-modules/cpu-assembly-sampler/meson.build @@ -0,0 +1,13 @@ +# +# See LICENSE for more information about licensing +# Copyright 2026 +# +# Author: Diego Avila +# + +lib_sampling_by_pid_headers = [] +if enable_sampling_by_pid + lib_sampling_by_pid_headers += [ + files('sampling-by-pid.hpp'), + ] +endif diff --git a/include/efimon/ebpf-modules/cpu-assembly-sampler/sampling-by-pid.hpp b/include/efimon/ebpf-modules/cpu-assembly-sampler/sampling-by-pid.hpp new file mode 100644 index 0000000..44eeebe --- /dev/null +++ b/include/efimon/ebpf-modules/cpu-assembly-sampler/sampling-by-pid.hpp @@ -0,0 +1,264 @@ +/** + * @file sampling-by-pid.hpp + * @author Diego Avila (diego.avila@uned.cr) + * @brief Sampling-by-PID eBPF-based CPU cycle sampler + * + * @copyright Copyright (c) 2026. See License for Licensing + */ + +#ifndef INCLUDE_EFIMON_EBPF_MODULES_CPU_ASSEMBLY_SAMPLER_SAMPLING_BY_PID_HPP_ +#define INCLUDE_EFIMON_EBPF_MODULES_CPU_ASSEMBLY_SAMPLER_SAMPLING_BY_PID_HPP_ + +#include +#include // NOLINT +#include // NOLINT +#include +#include +#include +#include +#include +#include +#include +#include +#include // NOLINT +#include +#include // NOLINT +#include + +namespace efimon { + +/** + * @brief Observer class that wraps the eBPF Sampling-by-PID interface and + * collects CPU cycle samples from a running process + * + * This observer uses eBPF programs to efficiently sample CPU cycles via + * perf events, collecting instruction pointers without the overhead of + * ptrace. The samples are decoded using the AsmClassifier. + */ +class SamplingByPIDObserver : public Observer { + public: + /** + * @brief Constructor for the Sampling-by-PID eBPF Observer + * + * @param pid process id to attach + * @param scope only ObserverScope::PROCESS is valid + * @param interval interval of how often the profiler is queried in + * milliseconds. 0 for manual query. + * @param frequency sampling frequency in Hz (how often CPU cycles are + * sampled). Default 10000 Hz. Capped at runtime to the kernel's + * perf_event_max_sample_rate. + */ + SamplingByPIDObserver(const uint pid = 0, + const ObserverScope = ObserverScope::PROCESS, + const uint64_t interval = 0, + const uint64_t frequency = 10000); + + /** + * @brief Manually triggers the update in case that there is no interval + * + * @return Status of the transaction + */ + Status Trigger() override; + + /** + * @brief Get the Readings from the Observer + * + * Before reading it, the interval must be finished or the + * Observer::Trigger() method must be invoked before calling this method + * + * @return std::vector vector of readings from the observer. + * In this case, the Readings* can be dynamic-casted to: + * + * The order will be 0: InstructionReadings + * + * More to be defined during the way + */ + std::vector GetReadings() override; + + /** + * @brief Select the device to measure: not used + * + * @param device device enumeration + * @return Status of the transaction + */ + Status SelectDevice(const uint device) override; + + /** + * @brief Set the Scope of the Observer instance (not implemented) + * + * @param scope instance scope, if it is process-specific or system-wide + * @return Status of the transaction + */ + Status SetScope(const ObserverScope scope) override; + + /** + * @brief Set the process PID + * + * @param pid process ID + * @return Status of the transaction + */ + Status SetPID(const uint pid) override; + + /** + * @brief Get the Scope of the Observer instance + * + * @return scope of the instance + */ + ObserverScope GetScope() const noexcept override; + + /** + * @brief Get the process ID in case of a process-specific instance + * + * @return process ID + */ + uint GetPID() const noexcept override; + + /** + * @brief Get the Capabilities of the Observer instance + * + * @return vector of capabilities + */ + const std::vector& GetCapabilities() + const noexcept override; + + /** + * @brief Get the Status of the Observer + * + * @return Status of the instance + */ + Status GetStatus() override; + + /** + * @brief Set the Interval in milliseconds + * + * Sets how often the observer will be refreshed + * + * @param interval time in milliseconds + * @return Status of the setting process + */ + Status SetInterval(const uint64_t interval) override; + + /** + * @brief Clear the interval + * + * Avoids the instance to be automatically refreshed + * + * @return Status + */ + Status ClearInterval() override; + + /** + * @brief Resets the instance + * + * The effect is quite similar to destroy and re-construct the instance + * + * @return Status + */ + Status Reset() override; + + /** + * @brief Destroy the Observer + */ + virtual ~SamplingByPIDObserver(); + + /** + * @brief Get the number of raw eBPF samples collected + */ + uint64_t GetCollectedSamplesCount() const noexcept { + return collected_samples_.size(); + } + + /** + * @brief Get the number of successfully decoded userspace samples + */ + uint64_t GetDecodedSamplesCount() const noexcept { + return samples_collected_; + } + + private: + /** Instruction readings: where the results are going to be encapsulated */ + InstructionReadings readings_; + + uint64_t frequency_; + /** If true, the instance has valid measurements */ + bool valid_; + /** Classifier to construct the proper histograms */ + std::unique_ptr classifier_; + /** Ring buffer file descriptor for receiving samples */ + int ring_buffer_fd_; + /** eBPF skeleton object (opaque pointer) */ + void* bpf_skeleton_; + /** Vector of perf event file descriptors */ + std::vector perf_fds_; + /** Instruction pointer from current sample */ + uint64_t current_ip_; + /** Process ID from current sample */ + uint32_t current_sample_tid_; + /** Timestamp from current sample */ + uint64_t current_sample_ts_; + /** Memory contents from the instruction at the given IP */ + uint8_t inst_mem_[16]; + /** Instruction string */ + std::string inst_; + /** Number of samples collected */ + uint64_t samples_collected_; + /** Raw samples collected from the eBPF ring buffer */ + struct CollectedSample { + uint32_t pid; + uint32_t tid; + uint64_t ip; + uint64_t ts; + }; + std::vector collected_samples_; + /** Threading for asynchronous execution */ + std::unique_ptr worker_thread_; + /** Mutex for synchronization and consistency */ + std::mutex worker_mutex_; + /** Condition variable to wait for the termination */ + std::condition_variable worker_cv_; + /** Flag variable to break the worker */ + std::atomic worker_running_; + + /** + * @brief Initialize eBPF program and perf events + */ + Status InitializeBPF(); + + /** + * @brief Cleanup eBPF resources + */ + Status CleanupBPF(); + + /** + * @brief Poll the ring buffer for new samples + */ + Status PollRingBuffer(); + + /** + * @brief Decode the instruction at the given IP + */ + Status DecodeInstruction(const uint64_t ip); + + /** Parses the annotation results */ + Status ParseResults(); + + /** Normalize the annotation results */ + Status NormalizeResults(); + + /** Worker to poll samples in an asynchronous way */ + void Worker(); + + /** Ring buffer callback needs access to ProcessSample */ + friend int handle_rb_event(void* ctx, void* data, size_t size); + + public: + /** + * @brief Process a single sample from the ring buffer + */ + Status ProcessSample(const uint32_t pid, const uint32_t tid, + const uint64_t ip, const uint64_t ts); +}; + +} /* namespace efimon */ + +#endif // INCLUDE_EFIMON_EBPF_MODULES_CPU_ASSEMBLY_SAMPLER_SAMPLING_BY_PID_HPP_ diff --git a/include/efimon/ebpf-modules/disk-io/disk-io.hpp b/include/efimon/ebpf-modules/disk-io/disk-io.hpp new file mode 100644 index 0000000..3f83956 --- /dev/null +++ b/include/efimon/ebpf-modules/disk-io/disk-io.hpp @@ -0,0 +1,232 @@ +/** + * @file disk-io.hpp + * @author Diego Avila (diego.avila@uned.cr) + * @brief Disk I/O eBPF-based event tracer + * + * @copyright Copyright (c) 2026. See License for Licensing + */ + +#ifndef INCLUDE_EFIMON_EBPF_MODULES_DISK_IO_DISK_IO_HPP_ +#define INCLUDE_EFIMON_EBPF_MODULES_DISK_IO_DISK_IO_HPP_ + +#include +#include // NOLINT +#include // NOLINT +#include +#include +#include +#include +#include +#include +#include // NOLINT +#include +#include // NOLINT +#include + +namespace efimon { + +/** + * @brief Object to hold disk I/O event readings + */ +class EBPFDiskObserver : public Readings { + public: + /** + * @brief Construct a new EBPFDiskObserver object + */ + EBPFDiskObserver() = default; + + /** + * @brief Get the type of readings + * + * @return Type of readings + */ + Type GetType() const noexcept override; + + /** + * @brief Store the readings in a CSV-compatible string format + * + * @return CSV string representation + */ + std::string ToString() const override; + + /** Process ID that issues the I/O read */ + uint32_t pid = 0; + /** File descriptor of the read */ + uint32_t fd = 0; +}; + +/** + * @brief Observer class that wraps the disk I/O eBPF interface and + * traces disk I/O events from processes + * + * This observer uses eBPF programs to efficiently trace disk I/O operations + * via tracepoints, collecting information about read operations without the + * overhead of ptrace. + */ +class ModuloIODiskObserver : public Observer { + public: + /** + * @brief Constructor for the disk I/O eBPF observer + * + * @param pid process id to trace (0 for all processes) + * @param scope only ObserverScope::PROCESS is valid + * @param interval interval of how often the tracer is queried in + * milliseconds. 0 for manual query. + */ + ModuloIODiskObserver(const uint pid = 0, + const ObserverScope = ObserverScope::PROCESS, + const uint64_t interval = 0); + + /** + * @brief Manually triggers the update in case that there is no interval + * + * @return Status of the transaction + */ + Status Trigger() override; + + /** + * @brief Get the Readings from the Observer + * + * Before reading it, the interval must be finished or the + * Observer::Trigger() method must be invoked before calling this method + * + * @return std::vector vector of readings from the observer. + * In this case, the Readings* can be dynamic-casted to EBPFDiskObserver. + */ + std::vector GetReadings() override; + + /** + * @brief Select the device to measure: not used + * + * @param device device enumeration + * @return Status of the transaction + */ + Status SelectDevice(const uint device) override; + + /** + * @brief Set the Scope of the Observer instance (not implemented) + * + * @param scope instance scope, if it is process-specific or system-wide + * @return Status of the transaction + */ + Status SetScope(const ObserverScope scope) override; + + /** + * @brief Set the process PID + * + * @param pid process ID (0 for all processes) + * @return Status of the transaction + */ + Status SetPID(const uint pid) override; + + /** + * @brief Get the Scope of the Observer instance + * + * @return scope of the instance + */ + ObserverScope GetScope() const noexcept override; + + /** + * @brief Get the process ID in case of a process-specific instance + * + * @return process ID + */ + uint GetPID() const noexcept override; + + /** + * @brief Get the Capabilities of the Observer instance + * + * @return vector of capabilities + */ + const std::vector& GetCapabilities() + const noexcept override; + + /** + * @brief Get the Status of the Observer + * + * @return Status of the instance + */ + Status GetStatus() override; + + /** + * @brief Set the Interval in milliseconds + * + * Sets how often the observer will be refreshed + * + * @param interval time in milliseconds + * @return Status of the setting process + */ + Status SetInterval(const uint64_t interval) override; + + /** + * @brief Clear the interval + * + * Avoids the instance to be automatically refreshed + * + * @return Status + */ + Status ClearInterval() override; + + /** + * @brief Resets the instance + * + * The effect is quite similar to destroy and re-construct the instance + * + * @return Status + */ + Status Reset() override; + + /** + * @brief Destroy the Observer + */ + virtual ~ModuloIODiskObserver(); + + private: + /** I/O event readings: where the results are going to be encapsulated */ + std::vector readings_; + + /** If true, the instance has valid measurements */ + bool valid_; + /** Ring buffer file descriptor for receiving events */ + int ring_buffer_fd_; + /** eBPF skeleton object (opaque pointer) */ + void* bpf_skeleton_; + /** Number of events collected */ + uint64_t events_collected_; + /** Threading for asynchronous execution */ + std::unique_ptr worker_thread_; + /** Mutex for synchronization and consistency */ + std::mutex worker_mutex_; + /** Condition variable to wait for the termination */ + std::condition_variable worker_cv_; + /** Flag variable to break the worker */ + std::atomic worker_running_; + + /** + * @brief Initialize eBPF program and tracepoints + */ + Status InitializeBPF(); + + /** + * @brief Cleanup eBPF resources + */ + Status CleanupBPF(); + + /** + * @brief Poll the ring buffer for new events + */ + Status PollRingBuffer(); + + /** + * @brief Process a single I/O event from the ring buffer + */ + Status ProcessIOEvent(const uint32_t pid, const uint32_t fd, + const uint64_t bytes_read, const char* comm); + + /** Worker to poll events in an asynchronous way */ + void Worker(); +}; + +} /* namespace efimon */ + +#endif // INCLUDE_EFIMON_EBPF_MODULES_DISK_IO_DISK_IO_HPP_ diff --git a/include/efimon/ebpf-modules/disk-io/meson.build b/include/efimon/ebpf-modules/disk-io/meson.build new file mode 100644 index 0000000..342e376 --- /dev/null +++ b/include/efimon/ebpf-modules/disk-io/meson.build @@ -0,0 +1,13 @@ +# +# See LICENSE for more information about licensing +# Copyright 2026 +# +# Author: Diego Avila +# + +lib_disk_io_headers = [] +if enable_disk_io + lib_disk_io_headers += [ + files('disk-io.hpp'), + ] +endif diff --git a/include/efimon/ebpf-modules/meson.build b/include/efimon/ebpf-modules/meson.build new file mode 100644 index 0000000..b96af82 --- /dev/null +++ b/include/efimon/ebpf-modules/meson.build @@ -0,0 +1,17 @@ +# +# See LICENSE for more information about licensing +# Copyright 2026 +# +# Author: Diego Avila +# + +# eBPF Module: Sampling by PID +subdir('cpu-assembly-sampler') + +# eBPF Module: Disk I/O Tracing +subdir('disk-io') + +# Concatenate all eBPF modules headers +lib_ebpf_modules_headers = [] +lib_ebpf_modules_headers += lib_sampling_by_pid_headers +lib_ebpf_modules_headers += lib_disk_io_headers diff --git a/include/efimon/logger.hpp b/include/efimon/logger.hpp index 551c713..ae5559d 100644 --- a/include/efimon/logger.hpp +++ b/include/efimon/logger.hpp @@ -9,14 +9,13 @@ #ifndef INCLUDE_EFIMON_LOGGER_HPP_ #define INCLUDE_EFIMON_LOGGER_HPP_ +#include #include #include #include #include #include -#include - namespace efimon { class Logger { public: diff --git a/include/efimon/logger/csv.hpp b/include/efimon/logger/csv.hpp index 7b5f750..0061fde 100644 --- a/include/efimon/logger/csv.hpp +++ b/include/efimon/logger/csv.hpp @@ -9,6 +9,8 @@ #ifndef INCLUDE_EFIMON_LOGGER_CSV_HPP_ #define INCLUDE_EFIMON_LOGGER_CSV_HPP_ +#include +#include #include #include #include @@ -16,9 +18,6 @@ #include #include -#include -#include - namespace efimon { class CSVLogger : public Logger { public: diff --git a/include/efimon/logger/sqlite.hpp b/include/efimon/logger/sqlite.hpp index 569ad60..61073cc 100644 --- a/include/efimon/logger/sqlite.hpp +++ b/include/efimon/logger/sqlite.hpp @@ -9,17 +9,16 @@ #ifndef INCLUDE_EFIMON_LOGGER_SQLITE_HPP_ #define INCLUDE_EFIMON_LOGGER_SQLITE_HPP_ +#include + +#include +#include #include #include #include #include #include -#include - -#include -#include - namespace efimon { class SQLiteLogger : public Logger { public: diff --git a/include/efimon/meson.build b/include/efimon/meson.build index b277b2d..2fea6a2 100644 --- a/include/efimon/meson.build +++ b/include/efimon/meson.build @@ -33,6 +33,9 @@ subdir('logger') subdir('perf') subdir('ptrace-capstone') +# eBPF Modules +subdir('ebpf-modules') + # Power Specifics subdir('power') @@ -48,6 +51,7 @@ lib_headers += lib_asm_classifier_headers lib_headers += lib_logger_headers lib_headers += lib_perf_headers lib_headers += lib_ptrace_capstone_headers +lib_headers += lib_ebpf_modules_headers lib_headers += lib_power_headers lib_headers += lib_proc_headers lib_headers += lib_readings_headers @@ -56,6 +60,8 @@ install_headers(lib_asm_classifier_headers, subdir : 'efimon/asm-classifier') install_headers(lib_logger_headers, subdir : 'efimon/logger') install_headers(lib_perf_headers, subdir : 'efimon/perf') install_headers(lib_ptrace_capstone_headers, subdir : 'efimon/ptrace-capstone') +install_headers(lib_sampling_by_pid_headers, subdir : 'efimon/ebpf-modules/sampling-by-pid') +install_headers(lib_disk_io_headers, subdir : 'efimon/ebpf-modules/disk-io') install_headers(lib_power_headers, subdir : 'efimon/power') install_headers(lib_proc_headers, subdir : 'efimon/proc') install_headers(lib_readings_headers, subdir : 'efimon/readings') diff --git a/include/efimon/observer.hpp b/include/efimon/observer.hpp index 1130ba3..b2e583c 100644 --- a/include/efimon/observer.hpp +++ b/include/efimon/observer.hpp @@ -9,11 +9,10 @@ #ifndef INCLUDE_EFIMON_OBSERVER_HPP_ #define INCLUDE_EFIMON_OBSERVER_HPP_ -#include - #include #include #include +#include namespace efimon { @@ -103,8 +102,8 @@ class Observer { * * @return vector of capabilities */ - virtual const std::vector& GetCapabilities() const - noexcept = 0; + virtual const std::vector& GetCapabilities() + const noexcept = 0; /** * @brief Get the Status of the Observer diff --git a/include/efimon/perf/annotate.hpp b/include/efimon/perf/annotate.hpp index c32951d..7b986c4 100644 --- a/include/efimon/perf/annotate.hpp +++ b/include/efimon/perf/annotate.hpp @@ -17,7 +17,7 @@ #include #include #include -#include +#include // NOLINT(build/c++17) #include #include #include @@ -110,8 +110,8 @@ class PerfAnnotateObserver : public Observer { * * @return vector of capabilities */ - const std::vector& GetCapabilities() const - noexcept override; + const std::vector& GetCapabilities() + const noexcept override; /** * @brief Get the Status of the Observer diff --git a/include/efimon/perf/record.hpp b/include/efimon/perf/record.hpp index 19c20c7..1ede263 100644 --- a/include/efimon/perf/record.hpp +++ b/include/efimon/perf/record.hpp @@ -15,7 +15,7 @@ #include #include #include -#include +#include // NOLINT(build/c++17) #include #include @@ -214,4 +214,4 @@ class PerfRecordObserver : public Observer { } /* namespace efimon */ -#endif /* INCLUDE_EFIMON_PERF_RECORD_HPP_ */ +#endif // INCLUDE_EFIMON_PERF_RECORD_HPP_ diff --git a/include/efimon/power/ipmi.hpp b/include/efimon/power/ipmi.hpp index bd7f678..306cc9a 100644 --- a/include/efimon/power/ipmi.hpp +++ b/include/efimon/power/ipmi.hpp @@ -109,8 +109,8 @@ class IPMIMeterObserver : public Observer { * * @return vector of capabilities */ - const std::vector& GetCapabilities() const - noexcept override; + const std::vector& GetCapabilities() + const noexcept override; /** * @brief Get the Status of the Observer diff --git a/include/efimon/proc-lister.hpp b/include/efimon/proc-lister.hpp index bd1b469..edcf39e 100644 --- a/include/efimon/proc-lister.hpp +++ b/include/efimon/proc-lister.hpp @@ -9,11 +9,10 @@ #ifndef INCLUDE_EFIMON_PROC_LISTER_HPP_ #define INCLUDE_EFIMON_PROC_LISTER_HPP_ +#include #include #include -#include - namespace efimon { /** diff --git a/include/efimon/proc/io.hpp b/include/efimon/proc/io.hpp index 62ff4f3..1f1b2df 100644 --- a/include/efimon/proc/io.hpp +++ b/include/efimon/proc/io.hpp @@ -9,13 +9,12 @@ #ifndef INCLUDE_EFIMON_PROC_IO_HPP_ #define INCLUDE_EFIMON_PROC_IO_HPP_ -#include - #include #include #include #include #include +#include namespace efimon { @@ -117,8 +116,8 @@ class ProcIOObserver : public Observer { * * @return vector of capabilities */ - const std::vector& GetCapabilities() const - noexcept override; + const std::vector& GetCapabilities() + const noexcept override; /** * @brief Get the Status of the Observer diff --git a/include/efimon/proc/list.hpp b/include/efimon/proc/list.hpp index 7400f97..89a7975 100644 --- a/include/efimon/proc/list.hpp +++ b/include/efimon/proc/list.hpp @@ -9,10 +9,9 @@ #ifndef INCLUDE_EFIMON_PROC_LIST_HPP_ #define INCLUDE_EFIMON_PROC_LIST_HPP_ -#include - #include #include +#include namespace efimon { diff --git a/include/efimon/proc/meminfo.hpp b/include/efimon/proc/meminfo.hpp index 37d586d..0116ac2 100644 --- a/include/efimon/proc/meminfo.hpp +++ b/include/efimon/proc/meminfo.hpp @@ -9,13 +9,12 @@ #ifndef INCLUDE_EFIMON_PROC_MEMINFO_HPP_ #define INCLUDE_EFIMON_PROC_MEMINFO_HPP_ -#include - #include #include #include #include #include +#include namespace efimon { @@ -119,8 +118,8 @@ class ProcMemInfoObserver : public Observer { * * @return vector of capabilities */ - const std::vector& GetCapabilities() const - noexcept override; + const std::vector& GetCapabilities() + const noexcept override; /** * @brief Get the Status of the Observer diff --git a/include/efimon/proc/net.hpp b/include/efimon/proc/net.hpp index 9f76f22..3b4845f 100644 --- a/include/efimon/proc/net.hpp +++ b/include/efimon/proc/net.hpp @@ -9,15 +9,14 @@ #ifndef INCLUDE_EFIMON_PROC_NET_HPP_ #define INCLUDE_EFIMON_PROC_NET_HPP_ -#include -#include -#include - #include #include #include #include #include +#include +#include +#include namespace efimon { @@ -106,8 +105,8 @@ class ProcNetObserver : public Observer { * * @return vector of capabilities */ - const std::vector& GetCapabilities() const - noexcept override; + const std::vector& GetCapabilities() + const noexcept override; /** * @brief Get the Status of the Observer diff --git a/include/efimon/proc/stat.hpp b/include/efimon/proc/stat.hpp index 199c637..ca34f12 100644 --- a/include/efimon/proc/stat.hpp +++ b/include/efimon/proc/stat.hpp @@ -9,14 +9,13 @@ #ifndef INCLUDE_EFIMON_PROC_STAT_HPP_ #define INCLUDE_EFIMON_PROC_STAT_HPP_ -#include - #include #include #include #include #include #include +#include #define MAX_NUM_CPUS 1024 @@ -171,8 +170,8 @@ class ProcStatObserver : public Observer { * * @return vector of capabilities */ - const std::vector& GetCapabilities() const - noexcept override; + const std::vector& GetCapabilities() + const noexcept override; /** * @brief Get the Status of the Observer diff --git a/include/efimon/proc/thread-tree.hpp b/include/efimon/proc/thread-tree.hpp index f365753..c107b6f 100644 --- a/include/efimon/proc/thread-tree.hpp +++ b/include/efimon/proc/thread-tree.hpp @@ -9,11 +9,10 @@ #ifndef INCLUDE_EFIMON_PROC_THREAD_TREE_HPP_ #define INCLUDE_EFIMON_PROC_THREAD_TREE_HPP_ +#include #include #include -#include - namespace efimon { /** diff --git a/include/efimon/process-manager.hpp b/include/efimon/process-manager.hpp index 07579bc..0a39686 100644 --- a/include/efimon/process-manager.hpp +++ b/include/efimon/process-manager.hpp @@ -9,13 +9,11 @@ #ifndef INCLUDE_EFIMON_PROCESS_MANAGER_HPP_ #define INCLUDE_EFIMON_PROCESS_MANAGER_HPP_ +#include #include #include -#include - -#include - #include +#include namespace efimon { diff --git a/include/efimon/readings/io-readings.hpp b/include/efimon/readings/io-readings.hpp index 664bfdd..b43a3d0 100644 --- a/include/efimon/readings/io-readings.hpp +++ b/include/efimon/readings/io-readings.hpp @@ -10,9 +10,8 @@ #define INCLUDE_EFIMON_READINGS_IO_READINGS_HPP_ #include -#include - #include +#include namespace efimon { diff --git a/include/efimon/readings/net-readings.hpp b/include/efimon/readings/net-readings.hpp index 9d282f5..c2eab31 100644 --- a/include/efimon/readings/net-readings.hpp +++ b/include/efimon/readings/net-readings.hpp @@ -10,11 +10,10 @@ #define INCLUDE_EFIMON_READINGS_NET_READINGS_HPP_ #include +#include #include #include -#include - namespace efimon { /** diff --git a/include/efimon/readings/ram-readings.hpp b/include/efimon/readings/ram-readings.hpp index d8c9a95..897de61 100644 --- a/include/efimon/readings/ram-readings.hpp +++ b/include/efimon/readings/ram-readings.hpp @@ -10,9 +10,8 @@ #define INCLUDE_EFIMON_READINGS_RAM_READINGS_HPP_ #include -#include - #include +#include namespace efimon { diff --git a/include/third-party/pstream.hpp b/include/third-party/pstream.hpp index c06c1b2..8155c40 100644 --- a/include/third-party/pstream.hpp +++ b/include/third-party/pstream.hpp @@ -19,32 +19,32 @@ #ifndef REDI_PSTREAM_H_SEEN #define REDI_PSTREAM_H_SEEN +#include // for ioctl() and FIONREAD +#include // for pid_t +#include // for waitpid() + +#include // for min() +#include // for errno +#include // for size_t, NULL +#include // for exit() #include -#include #include #include +#include #include #include -#include // for min() -#include // for errno -#include // for size_t, NULL -#include // for exit() -#include // for pid_t -#include // for waitpid() -#include // for ioctl() and FIONREAD #if defined(__sun) -# include // for FIONREAD on Solaris 2.5 +#include // for FIONREAD on Solaris 2.5 #endif -#include // for pipe() fork() exec() and filedes functions -#include // for kill() -#include // for fcntl() +#include // for fcntl() +#include // for kill() +#include // for pipe() fork() exec() and filedes functions #if REDI_EVISCERATE_PSTREAMS -# include // for FILE, fdopen() +#include // for FILE, fdopen() #endif - /// The library version. -#define PSTREAMS_VERSION 0x0103 // 1.0.3 +#define PSTREAMS_VERSION 0x0103 // 1.0.3 /** * @namespace redi @@ -59,2421 +59,2084 @@ * it is used internally to provide the common functionality for the * other stream classes. */ -namespace redi -{ - /// Common base class providing constants and typenames. - struct pstreams - { - /// Type used to specify how to connect to the process. - typedef std::ios_base::openmode pmode; +namespace redi { +/// Common base class providing constants and typenames. +struct pstreams { + /// Type used to specify how to connect to the process. + typedef std::ios_base::openmode pmode; - /// Type used to hold the arguments for a command. - typedef std::vector argv_type; + /// Type used to hold the arguments for a command. + typedef std::vector argv_type; - /// Type used for file descriptors. - typedef int fd_type; + /// Type used for file descriptors. + typedef int fd_type; - static const pmode pstdin = std::ios_base::out; ///< Write to stdin - static const pmode pstdout = std::ios_base::in; ///< Read from stdout - static const pmode pstderr = std::ios_base::app; ///< Read from stderr + static const pmode pstdin = std::ios_base::out; ///< Write to stdin + static const pmode pstdout = std::ios_base::in; ///< Read from stdout + static const pmode pstderr = std::ios_base::app; ///< Read from stderr - /// Create a new process group for the child process. - static const pmode newpg = std::ios_base::trunc; + /// Create a new process group for the child process. + static const pmode newpg = std::ios_base::trunc; - protected: - enum { - bufsz = 32, ///< Size of pstreambuf buffers. - pbsz = 2 ///< Number of putback characters kept. - }; + protected: + enum { + bufsz = 32, ///< Size of pstreambuf buffers. + pbsz = 2 ///< Number of putback characters kept. + }; #if __cplusplus >= 201103L - template - using stringable = decltype((void)std::string(std::declval())); + template + using stringable = decltype((void)std::string(std::declval())); #endif - }; - - /// Class template for stream buffer. - template > - class basic_pstreambuf - : public std::basic_streambuf - , public pstreams - { - public: - // Type definitions for dependent types - typedef CharT char_type; - typedef Traits traits_type; - typedef typename traits_type::int_type int_type; - typedef typename traits_type::off_type off_type; - typedef typename traits_type::pos_type pos_type; - /** @deprecated use pstreams::fd_type instead. */ - typedef fd_type fd_t; - - /// Default constructor. - basic_pstreambuf(); - - /// Constructor that initialises the buffer with @a cmd. - basic_pstreambuf(const std::string& cmd, pmode mode); - - /// Constructor that initialises the buffer with @a file and @a argv. - basic_pstreambuf( const std::string& file, - const argv_type& argv, - pmode mode ); +}; + +/// Class template for stream buffer. +template > +class basic_pstreambuf : public std::basic_streambuf, + public pstreams { + public: + // Type definitions for dependent types + typedef CharT char_type; + typedef Traits traits_type; + typedef typename traits_type::int_type int_type; + typedef typename traits_type::off_type off_type; + typedef typename traits_type::pos_type pos_type; + /** @deprecated use pstreams::fd_type instead. */ + typedef fd_type fd_t; + + /// Default constructor. + basic_pstreambuf(); + + /// Constructor that initialises the buffer with @a cmd. + basic_pstreambuf(const std::string& cmd, pmode mode); + + /// Constructor that initialises the buffer with @a file and @a argv. + basic_pstreambuf(const std::string& file, const argv_type& argv, pmode mode); #if __cplusplus >= 201103L - basic_pstreambuf(basic_pstreambuf&&) noexcept; - basic_pstreambuf& operator=(basic_pstreambuf&&) noexcept; - void swap(basic_pstreambuf&) noexcept; + basic_pstreambuf(basic_pstreambuf&&) noexcept; + basic_pstreambuf& operator=(basic_pstreambuf&&) noexcept; + void swap(basic_pstreambuf&) noexcept; #endif - /// Destructor. - ~basic_pstreambuf(); + /// Destructor. + ~basic_pstreambuf(); - /// Initialise the stream buffer with @a cmd. - basic_pstreambuf* - open(const std::string& cmd, pmode mode); + /// Initialise the stream buffer with @a cmd. + basic_pstreambuf* open(const std::string& cmd, pmode mode); - /// Initialise the stream buffer with @a file and @a argv. - basic_pstreambuf* - open(const std::string& file, const argv_type& argv, pmode mode); + /// Initialise the stream buffer with @a file and @a argv. + basic_pstreambuf* open(const std::string& file, const argv_type& argv, + pmode mode); - /// Close the stream buffer and wait for the process to exit. - basic_pstreambuf* - close(); + /// Close the stream buffer and wait for the process to exit. + basic_pstreambuf* close(); - /// Send a signal to the process. - basic_pstreambuf* - kill(int signal = SIGTERM); + /// Send a signal to the process. + basic_pstreambuf* kill(int signal = SIGTERM); - /// Send a signal to the process' process group. - basic_pstreambuf* - killpg(int signal = SIGTERM); + /// Send a signal to the process' process group. + basic_pstreambuf* killpg(int signal = SIGTERM); - /// Close the pipe connected to the process' stdin. - void - peof(); + /// Close the pipe connected to the process' stdin. + void peof(); - /// Change active input source. - bool - read_err(bool readerr = true); + /// Change active input source. + bool read_err(bool readerr = true); - /// Report whether the stream buffer has been initialised. - bool - is_open() const; + /// Report whether the stream buffer has been initialised. + bool is_open() const; - /// Report whether the process has exited. - bool - exited(); + /// Report whether the process has exited. + bool exited(); #if REDI_EVISCERATE_PSTREAMS - /// Obtain FILE pointers for each of the process' standard streams. - std::size_t - fopen(FILE*& in, FILE*& out, FILE*& err); + /// Obtain FILE pointers for each of the process' standard streams. + std::size_t fopen(FILE*& in, FILE*& out, FILE*& err); #endif - /// Return the exit status of the process. - int - status() const; + /// Return the exit status of the process. + int status() const; - /// Return the error number (errno) for the most recent failed operation. - int - error() const; + /// Return the error number (errno) for the most recent failed operation. + int error() const; - /// Gets the PID - pid_t - getpid(); + /// Gets the PID + pid_t getpid(); - protected: - /// Transfer characters to the pipe when character buffer overflows. - int_type - overflow(int_type c); + protected: + /// Transfer characters to the pipe when character buffer overflows. + int_type overflow(int_type c); - /// Transfer characters from the pipe when the character buffer is empty. - int_type - underflow(); + /// Transfer characters from the pipe when the character buffer is empty. + int_type underflow(); - /// Make a character available to be returned by the next extraction. - int_type - pbackfail(int_type c = traits_type::eof()); + /// Make a character available to be returned by the next extraction. + int_type pbackfail(int_type c = traits_type::eof()); - /// Write any buffered characters to the stream. - int - sync(); + /// Write any buffered characters to the stream. + int sync(); - /// Insert multiple characters into the pipe. - std::streamsize - xsputn(const char_type* s, std::streamsize n); + /// Insert multiple characters into the pipe. + std::streamsize xsputn(const char_type* s, std::streamsize n); - /// Insert a sequence of characters into the pipe. - std::streamsize - write(const char_type* s, std::streamsize n); + /// Insert a sequence of characters into the pipe. + std::streamsize write(const char_type* s, std::streamsize n); - /// Extract a sequence of characters from the pipe. - std::streamsize - read(char_type* s, std::streamsize n); + /// Extract a sequence of characters from the pipe. + std::streamsize read(char_type* s, std::streamsize n); - /// Report how many characters can be read from active input without blocking. - std::streamsize - showmanyc(); + /// Report how many characters can be read from active input without blocking. + std::streamsize showmanyc(); - protected: - /// Enumerated type to indicate whether stdout or stderr is to be read. - enum buf_read_src { rsrc_out = 0, rsrc_err = 1 }; + protected: + /// Enumerated type to indicate whether stdout or stderr is to be read. + enum buf_read_src { rsrc_out = 0, rsrc_err = 1 }; - /// Initialise pipes and fork process. - pid_t - fork(pmode mode); + /// Initialise pipes and fork process. + pid_t fork(pmode mode); - /// Wait for the child process to exit. - int - wait(bool nohang = false); + /// Wait for the child process to exit. + int wait(bool nohang = false); - /// Return the file descriptor for the output pipe. - fd_type& - wpipe(); + /// Return the file descriptor for the output pipe. + fd_type& wpipe(); - /// Return the file descriptor for the active input pipe. - fd_type& - rpipe(); + /// Return the file descriptor for the active input pipe. + fd_type& rpipe(); - /// Return the file descriptor for the specified input pipe. - fd_type& - rpipe(buf_read_src which); + /// Return the file descriptor for the specified input pipe. + fd_type& rpipe(buf_read_src which); - void - create_buffers(pmode mode); + void create_buffers(pmode mode); - void - destroy_buffers(pmode mode); + void destroy_buffers(pmode mode); - /// Writes buffered characters to the process' stdin pipe. - bool - empty_buffer(); + /// Writes buffered characters to the process' stdin pipe. + bool empty_buffer(); - bool - fill_buffer(bool non_blocking = false); + bool fill_buffer(bool non_blocking = false); - /// Return the active input buffer. - char_type* - rbuffer(); + /// Return the active input buffer. + char_type* rbuffer(); - buf_read_src - switch_read_buffer(buf_read_src); + buf_read_src switch_read_buffer(buf_read_src); - private: + private: #if __cplusplus >= 201103L - using basic_streambuf = std::basic_streambuf; + using basic_streambuf = std::basic_streambuf; #else - basic_pstreambuf(const basic_pstreambuf&); - basic_pstreambuf& operator=(const basic_pstreambuf&); + basic_pstreambuf(const basic_pstreambuf&); + basic_pstreambuf& operator=(const basic_pstreambuf&); #endif - void - init_rbuffers(); - - pid_t ppid_; // pid of process - fd_type wpipe_; // pipe used to write to process' stdin - fd_type rpipe_[2]; // two pipes to read from, stdout and stderr - char_type* wbuffer_; - char_type* rbuffer_[2]; - char_type* rbufstate_[3]; - /// Index into rpipe_[] to indicate active source for read operations. - buf_read_src rsrc_; - int status_; // hold exit status of child process - int error_; // hold errno if fork() or exec() fails - }; - - /// Class template for common base class. - template > - class pstream_common - : virtual public std::basic_ios - , virtual public pstreams - { - protected: - typedef basic_pstreambuf streambuf_type; - typedef std::basic_ios ios_type; - - typedef pstreams::pmode pmode; - typedef pstreams::argv_type argv_type; - - /// Default constructor. - pstream_common(); - - /// Constructor that initialises the stream by starting a process. - pstream_common(const std::string& cmd, pmode mode); - - /// Constructor that initialises the stream by starting a process. - pstream_common(const std::string& file, const argv_type& argv, pmode mode); - - /// Pure virtual destructor. - virtual - ~pstream_common() = 0; + void init_rbuffers(); -#if __cplusplus >= 201103L - pstream_common(pstream_common&& rhs) noexcept - : command_(std::move(rhs.command_)) - , buf_(std::move(rhs.buf_)) - { - /* derived class is responsible for ios_type::move(rhs) happening */ - } + pid_t ppid_; // pid of process + fd_type wpipe_; // pipe used to write to process' stdin + fd_type rpipe_[2]; // two pipes to read from, stdout and stderr + char_type* wbuffer_; + char_type* rbuffer_[2]; + char_type* rbufstate_[3]; + /// Index into rpipe_[] to indicate active source for read operations. + buf_read_src rsrc_; + int status_; // hold exit status of child process + int error_; // hold errno if fork() or exec() fails +}; - pstream_common& - operator=(pstream_common&& rhs) noexcept - { - command_ = std::move(rhs.command_); - buf_ = std::move(rhs.buf_); - return *this; - } +/// Class template for common base class. +template > +class pstream_common : virtual public std::basic_ios, + virtual public pstreams { + protected: + typedef basic_pstreambuf streambuf_type; + typedef std::basic_ios ios_type; - void - swap(pstream_common& rhs) noexcept - { - /* derived class is responsible for ios_type::swap(rhs) happening */ - command_.swap(rhs.command_); - buf_.swap(rhs.buf_); - } -#endif // C++11 + typedef pstreams::pmode pmode; + typedef pstreams::argv_type argv_type; - /// Start a process. - void - do_open(const std::string& cmd, pmode mode); + /// Default constructor. + pstream_common(); - /// Start a process. - void - do_open(const std::string& file, const argv_type& argv, pmode mode); + /// Constructor that initialises the stream by starting a process. + pstream_common(const std::string& cmd, pmode mode); - public: - /// Close the pipe, returning the program's exit status, as - /// pclose(3) does. - int - close(); + /// Constructor that initialises the stream by starting a process. + pstream_common(const std::string& file, const argv_type& argv, pmode mode); - /// Gets the PID - int - getpid(); + /// Pure virtual destructor. + virtual ~pstream_common() = 0; - /// Report whether the stream's buffer has been initialised. - bool - is_open() const; +#if __cplusplus >= 201103L + pstream_common(pstream_common&& rhs) noexcept + : command_(std::move(rhs.command_)), buf_(std::move(rhs.buf_)) { + /* derived class is responsible for ios_type::move(rhs) happening */ + } - /// Return the command used to initialise the stream. - const std::string& - command() const; + pstream_common& operator=(pstream_common&& rhs) noexcept { + command_ = std::move(rhs.command_); + buf_ = std::move(rhs.buf_); + return *this; + } - /// Return a pointer to the stream buffer. - streambuf_type* - rdbuf() const; + void swap(pstream_common& rhs) noexcept { + /* derived class is responsible for ios_type::swap(rhs) happening */ + command_.swap(rhs.command_); + buf_.swap(rhs.buf_); + } +#endif // C++11 -#if REDI_EVISCERATE_PSTREAMS - /// Obtain FILE pointers for each of the process' standard streams. - std::size_t - fopen(FILE*& in, FILE*& out, FILE*& err); -#endif + /// Start a process. + void do_open(const std::string& cmd, pmode mode); - protected: - std::string command_; ///< The command used to start the process. - streambuf_type buf_; ///< The stream buffer. - }; + /// Start a process. + void do_open(const std::string& file, const argv_type& argv, pmode mode); + public: + /// Close the pipe, returning the program's exit status, as + /// pclose(3) does. + int close(); - /** - * @class basic_ipstream - * @brief Class template for Input PStreams. - * - * Reading from an ipstream reads the command's standard output and/or - * standard error (depending on how the ipstream is opened) - * and the command's standard input is the same as that of the process - * that created the object, unless altered by the command itself. - */ + /// Gets the PID + int getpid(); - template > - class basic_ipstream - : public std::basic_istream - , public pstream_common - , virtual public pstreams - { - typedef std::basic_istream istream_type; - typedef pstream_common pbase_type; + /// Report whether the stream's buffer has been initialised. + bool is_open() const; - using pbase_type::buf_; // declare name in this scope + /// Return the command used to initialise the stream. + const std::string& command() const; - // Ensure a basic_ipstream will read from at least one pipe - pmode readable(pmode mode) - { - if (!(mode & (pstdout|pstderr))) - mode |= pstdout; - return mode; - } + /// Return a pointer to the stream buffer. + streambuf_type* rdbuf() const; - public: - /// Type used to specify how to connect to the process. - typedef typename pbase_type::pmode pmode; - - /// Type used to hold the arguments for a command. - typedef typename pbase_type::argv_type argv_type; - - /// Default constructor, creates an uninitialised stream. - basic_ipstream() - : istream_type(NULL), pbase_type() - { } - - /** - * @brief Constructor that initialises the stream by starting a process. - * - * Initialises the stream buffer by calling do_open() with the supplied - * arguments. - * - * @param cmd a string containing a shell command. - * @param mode the I/O mode to use when opening the pipe. - * @see do_open(const std::string&, pmode) - */ - explicit - basic_ipstream(const std::string& cmd, pmode mode = pstdout) - : istream_type(NULL), pbase_type(cmd, readable(mode)) - { } - - /** - * @brief Constructor that initialises the stream by starting a process. - * - * Initialises the stream buffer by calling do_open() with the supplied - * arguments. - * - * @param file a string containing the pathname of a program to execute. - * @param argv a vector of argument strings passed to the new program. - * @param mode the I/O mode to use when opening the pipe. - * @see do_open(const std::string&, const argv_type&, pmode) - */ - basic_ipstream( const std::string& file, - const argv_type& argv, - pmode mode = pstdout ) - : istream_type(NULL), pbase_type(file, argv, readable(mode)) - { } - - /** - * @brief Constructor that initialises the stream by starting a process. - * - * Initialises the stream buffer by calling - * @c do_open(argv[0],argv,mode|pstdout) - * - * @param argv a vector of argument strings passed to the new program. - * @param mode the I/O mode to use when opening the pipe. - * @see do_open(const std::string&, const argv_type&, pmode) - */ - explicit - basic_ipstream(const argv_type& argv, pmode mode = pstdout) - : istream_type(NULL), pbase_type(argv.at(0), argv, readable(mode)) - { } +#if REDI_EVISCERATE_PSTREAMS + /// Obtain FILE pointers for each of the process' standard streams. + std::size_t fopen(FILE*& in, FILE*& out, FILE*& err); +#endif -#if __cplusplus >= 201103L - template> - explicit - basic_ipstream(std::initializer_list args, pmode mode = pstdout) - : basic_ipstream(argv_type(args.begin(), args.end()), mode) - { } - - basic_ipstream(basic_ipstream&& rhs) - : istream_type(std::move(rhs)) - , pbase_type(std::move(rhs)) - { istream_type::set_rdbuf(std::addressof(pbase_type::buf_)); } + protected: + std::string command_; ///< The command used to start the process. + streambuf_type buf_; ///< The stream buffer. +}; - basic_ipstream& - operator=(basic_ipstream&& rhs) - { - istream_type::operator=(std::move(rhs)); - pbase_type::operator=(std::move(rhs)); - return *this; - } +/** + * @class basic_ipstream + * @brief Class template for Input PStreams. + * + * Reading from an ipstream reads the command's standard output and/or + * standard error (depending on how the ipstream is opened) + * and the command's standard input is the same as that of the process + * that created the object, unless altered by the command itself. + */ - void - swap(basic_ipstream& rhs) - { - istream_type::swap(rhs); - pbase_type::swap(rhs); - } -#endif // C++11 - - /** - * @brief Destructor. - * - * Closes the stream and waits for the child to exit. - */ - ~basic_ipstream() - { } - - /** - * @brief Start a process. - * - * Calls do_open( @a cmd , @a mode|pstdout ). - * - * @param cmd a string containing a shell command. - * @param mode the I/O mode to use when opening the pipe. - * @see do_open(const std::string&, pmode) - */ - void - open(const std::string& cmd, pmode mode = pstdout) - { - this->do_open(cmd, readable(mode)); - } +template > +class basic_ipstream : public std::basic_istream, + public pstream_common, + virtual public pstreams { + typedef std::basic_istream istream_type; + typedef pstream_common pbase_type; - /** - * @brief Start a process. - * - * Calls do_open( @a file , @a argv , @a mode|pstdout ). - * - * @param file a string containing the pathname of a program to execute. - * @param argv a vector of argument strings passed to the new program. - * @param mode the I/O mode to use when opening the pipe. - * @see do_open(const std::string&, const argv_type&, pmode) - */ - void - open( const std::string& file, - const argv_type& argv, - pmode mode = pstdout ) - { - this->do_open(file, argv, readable(mode)); - } + using pbase_type::buf_; // declare name in this scope - /** - * @brief Set streambuf to read from process' @c stdout. - * @return @c *this - */ - basic_ipstream& - out() - { - this->buf_.read_err(false); - return *this; - } + // Ensure a basic_ipstream will read from at least one pipe + pmode readable(pmode mode) { + if (!(mode & (pstdout | pstderr))) mode |= pstdout; + return mode; + } - /** - * @brief Set streambuf to read from process' @c stderr. - * @return @c *this - */ - basic_ipstream& - err() - { - this->buf_.read_err(true); - return *this; - } - }; + public: + /// Type used to specify how to connect to the process. + typedef typename pbase_type::pmode pmode; + + /// Type used to hold the arguments for a command. + typedef typename pbase_type::argv_type argv_type; + /// Default constructor, creates an uninitialised stream. + basic_ipstream() : istream_type(NULL), pbase_type() {} /** - * @class basic_opstream - * @brief Class template for Output PStreams. + * @brief Constructor that initialises the stream by starting a process. * - * Writing to an open opstream writes to the standard input of the command; - * the command's standard output is the same as that of the process that - * created the pstream object, unless altered by the command itself. + * Initialises the stream buffer by calling do_open() with the supplied + * arguments. + * + * @param cmd a string containing a shell command. + * @param mode the I/O mode to use when opening the pipe. + * @see do_open(const std::string&, pmode) */ - - template > - class basic_opstream - : public std::basic_ostream - , public pstream_common - , virtual public pstreams - { - typedef std::basic_ostream ostream_type; - typedef pstream_common pbase_type; - - using pbase_type::buf_; // declare name in this scope - - public: - /// Type used to specify how to connect to the process. - typedef typename pbase_type::pmode pmode; - - /// Type used to hold the arguments for a command. - typedef typename pbase_type::argv_type argv_type; - - /// Default constructor, creates an uninitialised stream. - basic_opstream() - : ostream_type(NULL), pbase_type() - { } - - /** - * @brief Constructor that initialises the stream by starting a process. - * - * Initialises the stream buffer by calling do_open() with the supplied - * arguments. - * - * @param cmd a string containing a shell command. - * @param mode the I/O mode to use when opening the pipe. - * @see do_open(const std::string&, pmode) - */ - explicit - basic_opstream(const std::string& cmd, pmode mode = pstdin) - : ostream_type(NULL), pbase_type(cmd, mode|pstdin) - { } - - /** - * @brief Constructor that initialises the stream by starting a process. - * - * Initialises the stream buffer by calling do_open() with the supplied - * arguments. - * - * @param file a string containing the pathname of a program to execute. - * @param argv a vector of argument strings passed to the new program. - * @param mode the I/O mode to use when opening the pipe. - * @see do_open(const std::string&, const argv_type&, pmode) - */ - basic_opstream( const std::string& file, - const argv_type& argv, - pmode mode = pstdin ) - : ostream_type(NULL), pbase_type(file, argv, mode|pstdin) - { } - - /** - * @brief Constructor that initialises the stream by starting a process. - * - * Initialises the stream buffer by calling - * @c do_open(argv[0],argv,mode|pstdin) - * - * @param argv a vector of argument strings passed to the new program. - * @param mode the I/O mode to use when opening the pipe. - * @see do_open(const std::string&, const argv_type&, pmode) - */ - explicit - basic_opstream(const argv_type& argv, pmode mode = pstdin) - : ostream_type(NULL), pbase_type(argv.at(0), argv, mode|pstdin) - { } - -#if __cplusplus >= 201103L - /** - * @brief Constructor that initialises the stream by starting a process. - * - * @param args a list of argument strings passed to the new program. - * @param mode the I/O mode to use when opening the pipe. - * @see do_open(const std::string&, const argv_type&, pmode) - */ - template> - explicit - basic_opstream(std::initializer_list args, pmode mode = pstdin) - : basic_opstream(argv_type(args.begin(), args.end()), mode) - { } - - basic_opstream(basic_opstream&& rhs) - : ostream_type(std::move(rhs)) - , pbase_type(std::move(rhs)) - { ostream_type::set_rdbuf(std::addressof(pbase_type::buf_)); } - - basic_opstream& - operator=(basic_opstream&& rhs) - { - ostream_type::operator=(std::move(rhs)); - pbase_type::operator=(std::move(rhs)); - return *this; - } - - void - swap(basic_opstream& rhs) - { - ostream_type::swap(rhs); - pbase_type::swap(rhs); - } -#endif // C++11 - - /** - * @brief Destructor - * - * Closes the stream and waits for the child to exit. - */ - ~basic_opstream() { } - - /** - * @brief Start a process. - * - * Calls do_open( @a cmd , @a mode|pstdin ). - * - * @param cmd a string containing a shell command. - * @param mode the I/O mode to use when opening the pipe. - * @see do_open(const std::string&, pmode) - */ - void - open(const std::string& cmd, pmode mode = pstdin) - { - this->do_open(cmd, mode|pstdin); - } - - /** - * @brief Start a process. - * - * Calls do_open( @a file , @a argv , @a mode|pstdin ). - * - * @param file a string containing the pathname of a program to execute. - * @param argv a vector of argument strings passed to the new program. - * @param mode the I/O mode to use when opening the pipe. - * @see do_open(const std::string&, const argv_type&, pmode) - */ - void - open( const std::string& file, - const argv_type& argv, - pmode mode = pstdin) - { - this->do_open(file, argv, mode|pstdin); - } - }; - + explicit basic_ipstream(const std::string& cmd, pmode mode = pstdout) + : istream_type(NULL), pbase_type(cmd, readable(mode)) {} /** - * @class basic_pstream - * @brief Class template for Bidirectional PStreams. + * @brief Constructor that initialises the stream by starting a process. + * + * Initialises the stream buffer by calling do_open() with the supplied + * arguments. * - * Writing to a pstream opened with @c pmode @c pstdin writes to the - * standard input of the command. - * Reading from a pstream opened with @c pmode @c pstdout and/or @c pstderr - * reads the command's standard output and/or standard error. - * Any of the process' @c stdin, @c stdout or @c stderr that is not - * connected to the pstream (as specified by the @c pmode) - * will be the same as the process that created the pstream object, - * unless altered by the command itself. + * @param file a string containing the pathname of a program to execute. + * @param argv a vector of argument strings passed to the new program. + * @param mode the I/O mode to use when opening the pipe. + * @see do_open(const std::string&, const argv_type&, pmode) */ - template > - class basic_pstream - : public std::basic_iostream - , public pstream_common - , virtual public pstreams - { - typedef std::basic_iostream iostream_type; - typedef pstream_common pbase_type; - - using pbase_type::buf_; // declare name in this scope - - public: - /// Type used to specify how to connect to the process. - typedef typename pbase_type::pmode pmode; - - /// Type used to hold the arguments for a command. - typedef typename pbase_type::argv_type argv_type; - - /// Default constructor, creates an uninitialised stream. - basic_pstream() - : iostream_type(NULL), pbase_type() - { } - - /** - * @brief Constructor that initialises the stream by starting a process. - * - * Initialises the stream buffer by calling do_open() with the supplied - * arguments. - * - * @param cmd a string containing a shell command. - * @param mode the I/O mode to use when opening the pipe. - * @see do_open(const std::string&, pmode) - */ - explicit - basic_pstream(const std::string& cmd, pmode mode = pstdout|pstdin) - : iostream_type(NULL), pbase_type(cmd, mode) - { } - - /** - * @brief Constructor that initialises the stream by starting a process. - * - * Initialises the stream buffer by calling do_open() with the supplied - * arguments. - * - * @param file a string containing the pathname of a program to execute. - * @param argv a vector of argument strings passed to the new program. - * @param mode the I/O mode to use when opening the pipe. - * @see do_open(const std::string&, const argv_type&, pmode) - */ - basic_pstream( const std::string& file, - const argv_type& argv, - pmode mode = pstdout|pstdin ) - : iostream_type(NULL), pbase_type(file, argv, mode) - { } - - /** - * @brief Constructor that initialises the stream by starting a process. - * - * Initialises the stream buffer by calling - * @c do_open(argv[0],argv,mode) - * - * @param argv a vector of argument strings passed to the new program. - * @param mode the I/O mode to use when opening the pipe. - * @see do_open(const std::string&, const argv_type&, pmode) - */ - explicit - basic_pstream(const argv_type& argv, pmode mode = pstdout|pstdin) - : iostream_type(NULL), pbase_type(argv.at(0), argv, mode) - { } - -#if __cplusplus >= 201103L - /** - * @brief Constructor that initialises the stream by starting a process. - * - * @param l a list of argument strings passed to the new program. - * @param mode the I/O mode to use when opening the pipe. - * @see do_open(const std::string&, const argv_type&, pmode) - */ - template> - explicit - basic_pstream(std::initializer_list l, pmode mode = pstdout|pstdin) - : basic_pstream(argv_type(l.begin(), l.end()), mode) - { } - - basic_pstream(basic_pstream&& rhs) - : iostream_type(std::move(rhs)) - , pbase_type(std::move(rhs)) - { iostream_type::set_rdbuf(std::addressof(pbase_type::buf_)); } - - basic_pstream& - operator=(basic_pstream&& rhs) - { - iostream_type::operator=(std::move(rhs)); - pbase_type::operator=(std::move(rhs)); - return *this; - } - - void - swap(basic_pstream& rhs) - { - iostream_type::swap(rhs); - pbase_type::swap(rhs); - } -#endif // C++11 - - /** - * @brief Destructor - * - * Closes the stream and waits for the child to exit. - */ - ~basic_pstream() { } - - /** - * @brief Start a process. - * - * Calls do_open( @a cnd , @a mode ). - * - * @param cmd a string containing a shell command. - * @param mode the I/O mode to use when opening the pipe. - * @see do_open(const std::string&, pmode) - */ - void - open(const std::string& cmd, pmode mode = pstdout|pstdin) - { - this->do_open(cmd, mode); - } - - /** - * @brief Start a process. - * - * Calls do_open( @a file , @a argv , @a mode ). - * - * @param file a string containing the pathname of a program to execute. - * @param argv a vector of argument strings passed to the new program. - * @param mode the I/O mode to use when opening the pipe. - * @see do_open(const std::string&, const argv_type&, pmode) - */ - void - open( const std::string& file, - const argv_type& argv, - pmode mode = pstdout|pstdin ) - { - this->do_open(file, argv, mode); - } - - /** - * @brief Set streambuf to read from process' @c stdout. - * @return @c *this - */ - basic_pstream& - out() - { - this->buf_.read_err(false); - return *this; - } - - /** - * @brief Set streambuf to read from process' @c stderr. - * @return @c *this - */ - basic_pstream& - err() - { - this->buf_.read_err(true); - return *this; - } - }; - + basic_ipstream(const std::string& file, const argv_type& argv, + pmode mode = pstdout) + : istream_type(NULL), pbase_type(file, argv, readable(mode)) {} /** - * @class basic_rpstream - * @brief Class template for Restricted PStreams. + * @brief Constructor that initialises the stream by starting a process. + * + * Initialises the stream buffer by calling + * @c do_open(argv[0],argv,mode|pstdout) * - * Writing to an rpstream opened with @c pmode @c pstdin writes to the - * standard input of the command. - * It is not possible to read directly from an rpstream object, to use - * an rpstream as in istream you must call either basic_rpstream::out() - * or basic_rpstream::err(). This is to prevent accidental reads from - * the wrong input source. If the rpstream was not opened with @c pmode - * @c pstderr then the class cannot read the process' @c stderr, and - * basic_rpstream::err() will return an istream that reads from the - * process' @c stdout, and vice versa. - * Reading from an rpstream opened with @c pmode @c pstdout and/or - * @c pstderr reads the command's standard output and/or standard error. - * Any of the process' @c stdin, @c stdout or @c stderr that is not - * connected to the pstream (as specified by the @c pmode) - * will be the same as the process that created the pstream object, - * unless altered by the command itself. + * @param argv a vector of argument strings passed to the new program. + * @param mode the I/O mode to use when opening the pipe. + * @see do_open(const std::string&, const argv_type&, pmode) */ - - template > - class basic_rpstream - : public std::basic_ostream - , private std::basic_istream - , private pstream_common - , virtual public pstreams - { - typedef std::basic_ostream ostream_type; - typedef std::basic_istream istream_type; - typedef pstream_common pbase_type; - - using pbase_type::buf_; // declare name in this scope - - public: - /// Type used to specify how to connect to the process. - typedef typename pbase_type::pmode pmode; - - /// Type used to hold the arguments for a command. - typedef typename pbase_type::argv_type argv_type; - - /// Default constructor, creates an uninitialised stream. - basic_rpstream() - : ostream_type(NULL), istream_type(NULL), pbase_type() - { } - - /** - * @brief Constructor that initialises the stream by starting a process. - * - * Initialises the stream buffer by calling do_open() with the supplied - * arguments. - * - * @param cmd a string containing a shell command. - * @param mode the I/O mode to use when opening the pipe. - * @see do_open(const std::string&, pmode) - */ - explicit - basic_rpstream(const std::string& cmd, pmode mode = pstdout|pstdin) - : ostream_type(NULL) , istream_type(NULL) , pbase_type(cmd, mode) - { } - - /** - * @brief Constructor that initialises the stream by starting a process. - * - * Initialises the stream buffer by calling do_open() with the supplied - * arguments. - * - * @param file a string containing the pathname of a program to execute. - * @param argv a vector of argument strings passed to the new program. - * @param mode the I/O mode to use when opening the pipe. - * @see do_open(const std::string&, const argv_type&, pmode) - */ - basic_rpstream( const std::string& file, - const argv_type& argv, - pmode mode = pstdout|pstdin ) - : ostream_type(NULL), istream_type(NULL), pbase_type(file, argv, mode) - { } - - /** - * @brief Constructor that initialises the stream by starting a process. - * - * Initialises the stream buffer by calling - * @c do_open(argv[0],argv,mode) - * - * @param argv a vector of argument strings passed to the new program. - * @param mode the I/O mode to use when opening the pipe. - * @see do_open(const std::string&, const argv_type&, pmode) - */ - explicit - basic_rpstream(const argv_type& argv, pmode mode = pstdout|pstdin) - : ostream_type(NULL), istream_type(NULL) - , pbase_type(argv.at(0), argv, mode) - { } + explicit basic_ipstream(const argv_type& argv, pmode mode = pstdout) + : istream_type(NULL), pbase_type(argv.at(0), argv, readable(mode)) {} #if __cplusplus >= 201103L - /** - * @brief Constructor that initialises the stream by starting a process. - * - * @param l a list of argument strings passed to the new program. - * @param mode the I/O mode to use when opening the pipe. - * @see do_open(const std::string&, const argv_type&, pmode) - */ - template> - explicit - basic_rpstream(std::initializer_list l, pmode mode = pstdout|pstdin) - : basic_rpstream(argv_type(l.begin(), l.end()), mode) - { } - - // TODO: figure out how to move istream and ostream bases separately, - // but so the virtual basic_ios base is only modified once. -#if 0 - basic_rpstream(basic_rpstream&& rhs) - : iostream_type(std::move(rhs)) - , pbase_type(std::move(rhs)) - { iostream_type::set_rdbuf(std::addressof(pbase_type::buf_)); } - - basic_rpstream& - operator=(basic_rpstream&& rhs) - { - iostream_type::operator=(std::move(rhs)); - pbase_type::operator=(std::move(rhs)); - return *this; - } - - void - swap(basic_rpstream& rhs) - { - iostream_type::swap(rhs); - pbase_type::swap(rhs); - } -#endif -#endif // C++11 - - /// Destructor - ~basic_rpstream() { } - - /** - * @brief Start a process. - * - * Calls do_open( @a cmd , @a mode ). - * - * @param cmd a string containing a shell command. - * @param mode the I/O mode to use when opening the pipe. - * @see do_open(const std::string&, pmode) - */ - void - open(const std::string& cmd, pmode mode = pstdout|pstdin) - { - this->do_open(cmd, mode); - } - - /** - * @brief Start a process. - * - * Calls do_open( @a file , @a argv , @a mode ). - * - * @param file a string containing the pathname of a program to execute. - * @param argv a vector of argument strings passed to the new program. - * @param mode the I/O mode to use when opening the pipe. - * @see do_open(const std::string&, const argv_type&, pmode) - */ - void - open( const std::string& file, - const argv_type& argv, - pmode mode = pstdout|pstdin ) - { - this->do_open(file, argv, mode); - } - - /** - * @brief Obtain a reference to the istream that reads - * the process' @c stdout. - * @return @c *this - */ - istream_type& - out() - { - this->buf_.read_err(false); - return *this; - } - - /** - * @brief Obtain a reference to the istream that reads - * the process' @c stderr. - * @return @c *this - */ - istream_type& - err() - { - this->buf_.read_err(true); - return *this; - } - }; + template > + explicit basic_ipstream(std::initializer_list args, pmode mode = pstdout) + : basic_ipstream(argv_type(args.begin(), args.end()), mode) {} + basic_ipstream(basic_ipstream&& rhs) + : istream_type(std::move(rhs)), pbase_type(std::move(rhs)) { + istream_type::set_rdbuf(std::addressof(pbase_type::buf_)); + } - /// Type definition for common template specialisation. - typedef basic_pstreambuf pstreambuf; - /// Type definition for common template specialisation. - typedef basic_ipstream ipstream; - /// Type definition for common template specialisation. - typedef basic_opstream opstream; - /// Type definition for common template specialisation. - typedef basic_pstream pstream; - /// Type definition for common template specialisation. - typedef basic_rpstream rpstream; + basic_ipstream& operator=(basic_ipstream&& rhs) { + istream_type::operator=(std::move(rhs)); + pbase_type::operator=(std::move(rhs)); + return *this; + } + void swap(basic_ipstream& rhs) { + istream_type::swap(rhs); + pbase_type::swap(rhs); + } +#endif // C++11 /** - * When inserted into an output pstream the manipulator calls - * basic_pstreambuf::peof() to close the output pipe, - * causing the child process to receive the end-of-file indicator - * on subsequent reads from its @c stdin stream. + * @brief Destructor. * - * @brief Manipulator to close the pipe connected to the process' stdin. - * @param s An output PStream class. - * @return The stream object the manipulator was invoked on. - * @see basic_pstreambuf::peof() - * @relates basic_opstream basic_pstream basic_rpstream + * Closes the stream and waits for the child to exit. */ - template - inline std::basic_ostream& - peof(std::basic_ostream& s) - { - typedef basic_pstreambuf pstreambuf_type; - if (pstreambuf_type* p = dynamic_cast(s.rdbuf())) - p->peof(); - return s; - } + ~basic_ipstream() {} + /** + * @brief Start a process. + * + * Calls do_open( @a cmd , @a mode|pstdout ). + * + * @param cmd a string containing a shell command. + * @param mode the I/O mode to use when opening the pipe. + * @see do_open(const std::string&, pmode) + */ + void open(const std::string& cmd, pmode mode = pstdout) { + this->do_open(cmd, readable(mode)); + } - /* - * member definitions for pstreambuf + /** + * @brief Start a process. + * + * Calls do_open( @a file , @a argv , @a mode|pstdout ). + * + * @param file a string containing the pathname of a program to execute. + * @param argv a vector of argument strings passed to the new program. + * @param mode the I/O mode to use when opening the pipe. + * @see do_open(const std::string&, const argv_type&, pmode) */ + void open(const std::string& file, const argv_type& argv, + pmode mode = pstdout) { + this->do_open(file, argv, readable(mode)); + } + /** + * @brief Set streambuf to read from process' @c stdout. + * @return @c *this + */ + basic_ipstream& out() { + this->buf_.read_err(false); + return *this; + } /** - * @class basic_pstreambuf - * Provides underlying streambuf functionality for the PStreams classes. + * @brief Set streambuf to read from process' @c stderr. + * @return @c *this */ + basic_ipstream& err() { + this->buf_.read_err(true); + return *this; + } +}; - /** Creates an uninitialised stream buffer. */ - template - inline - basic_pstreambuf::basic_pstreambuf() - : ppid_(-1) // initialise to -1 to indicate no process run yet. - , wpipe_(-1) - , wbuffer_() - , rbuffer_() - , rbufstate_() - , rsrc_(rsrc_out) - , status_(-1) - , error_(0) - { - rpipe_[rsrc_out] = rpipe_[rsrc_err] = -1; - } +/** + * @class basic_opstream + * @brief Class template for Output PStreams. + * + * Writing to an open opstream writes to the standard input of the command; + * the command's standard output is the same as that of the process that + * created the pstream object, unless altered by the command itself. + */ + +template > +class basic_opstream : public std::basic_ostream, + public pstream_common, + virtual public pstreams { + typedef std::basic_ostream ostream_type; + typedef pstream_common pbase_type; + + using pbase_type::buf_; // declare name in this scope + + public: + /// Type used to specify how to connect to the process. + typedef typename pbase_type::pmode pmode; + + /// Type used to hold the arguments for a command. + typedef typename pbase_type::argv_type argv_type; + + /// Default constructor, creates an uninitialised stream. + basic_opstream() : ostream_type(NULL), pbase_type() {} /** - * Initialises the stream buffer by calling open() with the supplied + * @brief Constructor that initialises the stream by starting a process. + * + * Initialises the stream buffer by calling do_open() with the supplied * arguments. * * @param cmd a string containing a shell command. * @param mode the I/O mode to use when opening the pipe. - * @see open() + * @see do_open(const std::string&, pmode) */ - template - inline - basic_pstreambuf::basic_pstreambuf(const std::string& cmd, pmode mode) - : ppid_(-1) // initialise to -1 to indicate no process run yet. - , wpipe_(-1) - , wbuffer_() - , rbuffer_() - , rbufstate_() - , rsrc_(rsrc_out) - , status_(-1) - , error_(0) - { - rpipe_[rsrc_out] = rpipe_[rsrc_err] = -1; - open(cmd, mode); - } + explicit basic_opstream(const std::string& cmd, pmode mode = pstdin) + : ostream_type(NULL), pbase_type(cmd, mode | pstdin) {} /** - * Initialises the stream buffer by calling open() with the supplied + * @brief Constructor that initialises the stream by starting a process. + * + * Initialises the stream buffer by calling do_open() with the supplied * arguments. * - * @param file a string containing the name of a program to execute. - * @param argv a vector of argument strings passsed to the new program. + * @param file a string containing the pathname of a program to execute. + * @param argv a vector of argument strings passed to the new program. * @param mode the I/O mode to use when opening the pipe. - * @see open() + * @see do_open(const std::string&, const argv_type&, pmode) */ - template - inline - basic_pstreambuf::basic_pstreambuf( const std::string& file, - const argv_type& argv, - pmode mode ) - : ppid_(-1) // initialise to -1 to indicate no process run yet. - , wpipe_(-1) - , wbuffer_() - , rbuffer_() - , rbufstate_() - , rsrc_(rsrc_out) - , status_(-1) - , error_(0) - { - rpipe_[rsrc_out] = rpipe_[rsrc_err] = -1; - open(file, argv, mode); - } + basic_opstream(const std::string& file, const argv_type& argv, + pmode mode = pstdin) + : ostream_type(NULL), pbase_type(file, argv, mode | pstdin) {} /** - * Closes the stream by calling close(). - * @see close() + * @brief Constructor that initialises the stream by starting a process. + * + * Initialises the stream buffer by calling + * @c do_open(argv[0],argv,mode|pstdin) + * + * @param argv a vector of argument strings passed to the new program. + * @param mode the I/O mode to use when opening the pipe. + * @see do_open(const std::string&, const argv_type&, pmode) */ - template - inline - basic_pstreambuf::~basic_pstreambuf() - { - close(); - } + explicit basic_opstream(const argv_type& argv, pmode mode = pstdin) + : ostream_type(NULL), pbase_type(argv.at(0), argv, mode | pstdin) {} #if __cplusplus >= 201103L /** - * Move constructor. - */ - template - inline - basic_pstreambuf::basic_pstreambuf( basic_pstreambuf&& rhs ) noexcept - : basic_streambuf(static_cast(rhs)) - , ppid_(rhs.ppid_) - , wpipe_(rhs.wpipe_) - , rpipe_{rhs.rpipe_[0], rhs.rpipe_[1]} - , wbuffer_(rhs.wbuffer_) - , rbuffer_{rhs.rbuffer_[0], rhs.rbuffer_[1]} - , rbufstate_{rhs.rbufstate_[0], rhs.rbufstate_[1], rhs.rbufstate_[2]} - , rsrc_(rhs.rsrc_) - , status_(rhs.status_) - , error_(rhs.error_) - { - rhs.ppid_ = -1; - rhs.wpipe_ = -1; - rhs.rpipe_[0] = rhs.rpipe_[1] = -1; - rhs.wbuffer_ = nullptr; - rhs.rbuffer_[0] = rhs.rbuffer_[1] = nullptr; - rhs.rbufstate_[0] = rhs.rbufstate_[1] = rhs.rbufstate_[2] = nullptr; - rhs.rsrc_ = rsrc_out; - rhs.status_ = -1; - rhs.error_ = 0; - rhs.setg(nullptr, nullptr, nullptr); - rhs.setp(nullptr, nullptr); - } - - template - inline basic_pstreambuf& - basic_pstreambuf::operator=( basic_pstreambuf&& rhs ) noexcept - { - close(); - basic_streambuf::operator=(static_cast(rhs)); - swap(rhs); - return *this; - } - - template - inline void - basic_pstreambuf::swap( basic_pstreambuf& rhs ) noexcept - { - basic_streambuf::swap(static_cast(rhs)); - std::swap(ppid_, rhs.ppid_); - std::swap(wpipe_, rhs.wpipe_); - std::swap(rpipe_, rhs.rpipe_); - std::swap(wbuffer_, rhs.wbuffer_); - std::swap(rbuffer_, rhs.rbuffer_); - std::swap(rbufstate_, rhs.rbufstate_); - std::swap(rsrc_, rhs.rsrc_); - std::swap(status_, rhs.status_); - std::swap(error_, rhs.error_); - } -#endif // C++11 - - /** - * Starts a new process by passing @a command to the shell (/bin/sh) - * and opens pipes to the process with the specified @a mode. - * - * If @a mode contains @c pstdout the initial read source will be - * the child process' stdout, otherwise if @a mode contains @c pstderr - * the initial read source will be the child's stderr. + * @brief Constructor that initialises the stream by starting a process. * - * Will duplicate the actions of the shell in searching for an - * executable file if the specified file name does not contain a slash (/) - * character. - * - * @warning - * There is no way to tell whether the shell command succeeded, this - * function will always succeed unless resource limits (such as - * memory usage, or number of processes or open files) are exceeded. - * This means is_open() will return true even if @a command cannot - * be executed. - * Use pstreambuf::open(const std::string&, const argv_type&, pmode) - * if you need to know whether the command failed to execute. - * - * @param command a string containing a shell command. - * @param mode a bitwise OR of one or more of @c out, @c in, @c err. - * @return NULL if the shell could not be started or the - * pipes could not be opened, @c this otherwise. - * @see execl(3) + * @param args a list of argument strings passed to the new program. + * @param mode the I/O mode to use when opening the pipe. + * @see do_open(const std::string&, const argv_type&, pmode) */ - template - basic_pstreambuf* - basic_pstreambuf::open(const std::string& command, pmode mode) - { - const char * shell_path = "/bin/sh"; -#if 0 - const std::string argv[] = { "sh", "-c", command }; - return this->open(shell_path, argv_type(argv, argv+3), mode); -#else - basic_pstreambuf* ret = NULL; - - if (!is_open()) - { - switch(fork(mode)) - { - case 0 : - // this is the new process, exec command - ::execl(shell_path, "sh", "-c", command.c_str(), (char*)NULL); + template > + explicit basic_opstream(std::initializer_list args, pmode mode = pstdin) + : basic_opstream(argv_type(args.begin(), args.end()), mode) {} - // can only reach this point if exec() failed - - // parent can get exit code from waitpid() - ::_exit(errno); - // using std::exit() would make static dtors run twice + basic_opstream(basic_opstream&& rhs) + : ostream_type(std::move(rhs)), pbase_type(std::move(rhs)) { + ostream_type::set_rdbuf(std::addressof(pbase_type::buf_)); + } - case -1 : - // couldn't fork, error already handled in pstreambuf::fork() - break; + basic_opstream& operator=(basic_opstream&& rhs) { + ostream_type::operator=(std::move(rhs)); + pbase_type::operator=(std::move(rhs)); + return *this; + } - default : - // this is the parent process - // activate buffers - create_buffers(mode); - ret = this; - } - } - return ret; -#endif - } + void swap(basic_opstream& rhs) { + ostream_type::swap(rhs); + pbase_type::swap(rhs); + } +#endif // C++11 /** - * @brief Helper function to close a file descriptor. - * - * Inspects @a fd and calls close(3) if it has a non-negative value. + * @brief Destructor * - * @param fd a file descriptor. - * @relates basic_pstreambuf + * Closes the stream and waits for the child to exit. */ - inline void - close_fd(pstreams::fd_type& fd) - { - if (fd >= 0 && ::close(fd) == 0) - fd = -1; - } + ~basic_opstream() {} /** - * @brief Helper function to close an array of file descriptors. + * @brief Start a process. * - * Calls @c close_fd() on each member of the array. - * The length of the array is determined automatically by - * template argument deduction to avoid errors. + * Calls do_open( @a cmd , @a mode|pstdin ). * - * @param fds an array of file descriptors. - * @relates basic_pstreambuf + * @param cmd a string containing a shell command. + * @param mode the I/O mode to use when opening the pipe. + * @see do_open(const std::string&, pmode) */ - template - inline void - close_fd_array(pstreams::fd_type (&fds)[N]) - { - for (std::size_t i = 0; i < N; ++i) - close_fd(fds[i]); - } + void open(const std::string& cmd, pmode mode = pstdin) { + this->do_open(cmd, mode | pstdin); + } /** - * Starts a new process by executing @a file with the arguments in - * @a argv and opens pipes to the process with the specified @a mode. + * @brief Start a process. * - * By convention @c argv[0] should be the file name of the file being - * executed. + * Calls do_open( @a file , @a argv , @a mode|pstdin ). * - * If @a mode contains @c pstdout the initial read source will be - * the child process' stdout, otherwise if @a mode contains @c pstderr - * the initial read source will be the child's stderr. - * - * Will duplicate the actions of the shell in searching for an - * executable file if the specified file name does not contain a slash (/) - * character. - * - * Iff @a file is successfully executed then is_open() will return true. - * Otherwise, pstreambuf::error() can be used to obtain the value of - * @c errno that was set by execvp(3) in the child process. - * - * The exit status of the new process will be returned by - * pstreambuf::status() after pstreambuf::exited() returns true. - * - * @param file a string containing the pathname of a program to execute. - * @param argv a vector of argument strings passed to the new program. - * @param mode a bitwise OR of one or more of @c out, @c in and @c err. - * @return NULL if a pipe could not be opened or if the program could - * not be executed, @c this otherwise. - * @see execvp(3) + * @param file a string containing the pathname of a program to execute. + * @param argv a vector of argument strings passed to the new program. + * @param mode the I/O mode to use when opening the pipe. + * @see do_open(const std::string&, const argv_type&, pmode) */ - template - basic_pstreambuf* - basic_pstreambuf::open( const std::string& file, - const argv_type& argv, - pmode mode ) - { - basic_pstreambuf* ret = NULL; - - if (!is_open()) - { - // constants for read/write ends of pipe - enum { RD, WR }; - - // open another pipe and set close-on-exec - fd_type ck_exec[] = { -1, -1 }; - if (-1 == ::pipe(ck_exec) - || -1 == ::fcntl(ck_exec[RD], F_SETFD, FD_CLOEXEC) - || -1 == ::fcntl(ck_exec[WR], F_SETFD, FD_CLOEXEC)) - { - error_ = errno; - close_fd_array(ck_exec); - } - else - { - switch(fork(mode)) - { - case 0 : - // this is the new process, exec command - { - char** arg_v = new char*[argv.size()+1]; - for (std::size_t i = 0; i < argv.size(); ++i) - { - const std::string& src = argv[i]; - char*& dest = arg_v[i]; - dest = new char[src.size()+1]; - dest[ src.copy(dest, src.size()) ] = '\0'; - } - arg_v[argv.size()] = NULL; - - ::execvp(file.c_str(), arg_v); - - // can only reach this point if exec() failed - - // parent can get error code from ck_exec pipe - error_ = errno; - - while (::write(ck_exec[WR], &error_, sizeof(error_)) == -1 - && errno == EINTR) - { } + void open(const std::string& file, const argv_type& argv, + pmode mode = pstdin) { + this->do_open(file, argv, mode | pstdin); + } +}; - ::close(ck_exec[WR]); - ::close(ck_exec[RD]); +/** + * @class basic_pstream + * @brief Class template for Bidirectional PStreams. + * + * Writing to a pstream opened with @c pmode @c pstdin writes to the + * standard input of the command. + * Reading from a pstream opened with @c pmode @c pstdout and/or @c pstderr + * reads the command's standard output and/or standard error. + * Any of the process' @c stdin, @c stdout or @c stderr that is not + * connected to the pstream (as specified by the @c pmode) + * will be the same as the process that created the pstream object, + * unless altered by the command itself. + */ +template > +class basic_pstream : public std::basic_iostream, + public pstream_common, + virtual public pstreams { + typedef std::basic_iostream iostream_type; + typedef pstream_common pbase_type; - ::_exit(error_); - // using std::exit() would make static dtors run twice - } + using pbase_type::buf_; // declare name in this scope - case -1 : - // couldn't fork, error already handled in pstreambuf::fork() - close_fd_array(ck_exec); - break; + public: + /// Type used to specify how to connect to the process. + typedef typename pbase_type::pmode pmode; - default : - // this is the parent process + /// Type used to hold the arguments for a command. + typedef typename pbase_type::argv_type argv_type; - // check child called exec() successfully - ::close(ck_exec[WR]); - switch (::read(ck_exec[RD], &error_, sizeof(error_))) - { - case 0: - // activate buffers - create_buffers(mode); - ret = this; - break; - case -1: - error_ = errno; - break; - default: - // error_ contains error code from child - // call wait() to clean up and set ppid_ to 0 - this->wait(); - break; - } - ::close(ck_exec[RD]); - } - } - } - return ret; - } + /// Default constructor, creates an uninitialised stream. + basic_pstream() : iostream_type(NULL), pbase_type() {} /** - * Gets the PID - * @return pid_t + * @brief Constructor that initialises the stream by starting a process. + * + * Initialises the stream buffer by calling do_open() with the supplied + * arguments. + * + * @param cmd a string containing a shell command. + * @param mode the I/O mode to use when opening the pipe. + * @see do_open(const std::string&, pmode) */ - template - pid_t - basic_pstreambuf::getpid() - { - return ppid_; - } + explicit basic_pstream(const std::string& cmd, pmode mode = pstdout | pstdin) + : iostream_type(NULL), pbase_type(cmd, mode) {} /** - * Creates pipes as specified by @a mode and calls @c fork() to create - * a new process. If the fork is successful the parent process stores - * the child's PID and the opened pipes and the child process replaces - * its standard streams with the opened pipes. + * @brief Constructor that initialises the stream by starting a process. * - * If an error occurs the error code will be set to one of the possible - * errors for @c pipe() or @c fork(). - * See your system's documentation for these error codes. + * Initialises the stream buffer by calling do_open() with the supplied + * arguments. * - * @param mode an OR of pmodes specifying which of the child's - * standard streams to connect to. - * @return On success the PID of the child is returned in the parent's - * context and zero is returned in the child's context. - * On error -1 is returned and the error code is set appropriately. + * @param file a string containing the pathname of a program to execute. + * @param argv a vector of argument strings passed to the new program. + * @param mode the I/O mode to use when opening the pipe. + * @see do_open(const std::string&, const argv_type&, pmode) */ - template - pid_t - basic_pstreambuf::fork(pmode mode) - { - pid_t pid = -1; - - // Three pairs of file descriptors, for pipes connected to the - // process' stdin, stdout and stderr - // (stored in a single array so close_fd_array() can close all at once) - fd_type fd[] = { -1, -1, -1, -1, -1, -1 }; - fd_type* const pin = fd; - fd_type* const pout = fd+2; - fd_type* const perr = fd+4; - - // constants for read/write ends of pipe - enum { RD, WR }; - - // N.B. - // For the pstreambuf pin is an output stream and - // pout and perr are input streams. - - if (!error_ && mode&pstdin && ::pipe(pin)) - error_ = errno; - - if (!error_ && mode&pstdout && ::pipe(pout)) - error_ = errno; - - if (!error_ && mode&pstderr && ::pipe(perr)) - error_ = errno; - - if (!error_) - { - pid = ::fork(); - switch (pid) - { - case 0 : - { - // this is the new process - - // for each open pipe close one end and redirect the - // respective standard stream to the other end - - if (*pin >= 0) - { - ::close(pin[WR]); - ::dup2(pin[RD], STDIN_FILENO); - ::close(pin[RD]); - } - if (*pout >= 0) - { - ::close(pout[RD]); - ::dup2(pout[WR], STDOUT_FILENO); - ::close(pout[WR]); - } - if (*perr >= 0) - { - ::close(perr[RD]); - ::dup2(perr[WR], STDERR_FILENO); - ::close(perr[WR]); - } - -#ifdef _POSIX_JOB_CONTROL - if (mode&newpg) - ::setpgid(0, 0); // Change to a new process group -#endif - - break; - } - case -1 : - { - // couldn't fork for some reason - error_ = errno; - // close any open pipes - close_fd_array(fd); - break; - } - default : - { - // this is the parent process, store process' pid - ppid_ = pid; - - // store one end of open pipes and close other end - if (*pin >= 0) - { - wpipe_ = pin[WR]; - ::close(pin[RD]); - } - if (*pout >= 0) - { - rpipe_[rsrc_out] = pout[RD]; - ::close(pout[WR]); - } - if (*perr >= 0) - { - rpipe_[rsrc_err] = perr[RD]; - ::close(perr[WR]); - } - } - } - } - else - { - // close any pipes we opened before failure - close_fd_array(fd); - } - return pid; - } + basic_pstream(const std::string& file, const argv_type& argv, + pmode mode = pstdout | pstdin) + : iostream_type(NULL), pbase_type(file, argv, mode) {} /** - * Closes all pipes and calls wait() to wait for the process to finish. - * If an error occurs the error code will be set to one of the possible - * errors for @c waitpid(). - * See your system's documentation for these errors. + * @brief Constructor that initialises the stream by starting a process. + * + * Initialises the stream buffer by calling + * @c do_open(argv[0],argv,mode) * - * @return @c this on successful close or @c NULL if there is no - * process to close or if an error occurs. + * @param argv a vector of argument strings passed to the new program. + * @param mode the I/O mode to use when opening the pipe. + * @see do_open(const std::string&, const argv_type&, pmode) */ - template - basic_pstreambuf* - basic_pstreambuf::close() - { - const bool running = is_open(); - - basic_pstreambuf::sync(); // might call wait() and reap the child process - - // rather than trying to work out whether or not we need to clean up - // just do it anyway, all cleanup functions are safe to call twice. - - destroy_buffers(pstdin|pstdout|pstderr); - - // close pipes before wait() so child gets EOF/SIGPIPE - close_fd(wpipe_); - close_fd_array(rpipe_); - - do - { - error_ = 0; - } while (wait() == -1 && error() == EINTR); - - return running ? this : NULL; - } + explicit basic_pstream(const argv_type& argv, pmode mode = pstdout | pstdin) + : iostream_type(NULL), pbase_type(argv.at(0), argv, mode) {} +#if __cplusplus >= 201103L /** - * Used to be called on construction to initialise the arrays for reading. - * No longer used. + * @brief Constructor that initialises the stream by starting a process. + * + * @param l a list of argument strings passed to the new program. + * @param mode the I/O mode to use when opening the pipe. + * @see do_open(const std::string&, const argv_type&, pmode) */ - template -#if __cplusplus >= 201402L && __has_cpp_attribute(deprecated) - [[deprecated]] -#elif __GNUC__ - __attribute__((deprecated)) -#endif - inline void - basic_pstreambuf::init_rbuffers() - { - rpipe_[rsrc_out] = rpipe_[rsrc_err] = -1; - rbuffer_[rsrc_out] = rbuffer_[rsrc_err] = NULL; - rbufstate_[0] = rbufstate_[1] = rbufstate_[2] = NULL; - } - - template - void - basic_pstreambuf::create_buffers(pmode mode) - { - if (mode & pstdin) - { - delete[] wbuffer_; - wbuffer_ = new char_type[bufsz]; - this->setp(wbuffer_, wbuffer_ + bufsz); - } - if (mode & pstdout) - { - delete[] rbuffer_[rsrc_out]; - rbuffer_[rsrc_out] = new char_type[bufsz]; - rsrc_ = rsrc_out; - this->setg(rbuffer_[rsrc_out] + pbsz, rbuffer_[rsrc_out] + pbsz, - rbuffer_[rsrc_out] + pbsz); - } - if (mode & pstderr) - { - delete[] rbuffer_[rsrc_err]; - rbuffer_[rsrc_err] = new char_type[bufsz]; - if (!(mode & pstdout)) - { - rsrc_ = rsrc_err; - this->setg(rbuffer_[rsrc_err] + pbsz, rbuffer_[rsrc_err] + pbsz, - rbuffer_[rsrc_err] + pbsz); - } - } - } + template > + explicit basic_pstream(std::initializer_list l, + pmode mode = pstdout | pstdin) + : basic_pstream(argv_type(l.begin(), l.end()), mode) {} + + basic_pstream(basic_pstream&& rhs) + : iostream_type(std::move(rhs)), pbase_type(std::move(rhs)) { + iostream_type::set_rdbuf(std::addressof(pbase_type::buf_)); + } - template - void - basic_pstreambuf::destroy_buffers(pmode mode) - { - if (mode & pstdin) - { - this->setp(NULL, NULL); - delete[] wbuffer_; - wbuffer_ = NULL; - } - if (mode & pstdout) - { - if (rsrc_ == rsrc_out) - this->setg(NULL, NULL, NULL); - delete[] rbuffer_[rsrc_out]; - rbuffer_[rsrc_out] = NULL; - } - if (mode & pstderr) - { - if (rsrc_ == rsrc_err) - this->setg(NULL, NULL, NULL); - delete[] rbuffer_[rsrc_err]; - rbuffer_[rsrc_err] = NULL; - } - } + basic_pstream& operator=(basic_pstream&& rhs) { + iostream_type::operator=(std::move(rhs)); + pbase_type::operator=(std::move(rhs)); + return *this; + } - template - typename basic_pstreambuf::buf_read_src - basic_pstreambuf::switch_read_buffer(buf_read_src src) - { - if (rsrc_ != src) - { - char_type* tmpbufstate[] = {this->eback(), this->gptr(), this->egptr()}; - this->setg(rbufstate_[0], rbufstate_[1], rbufstate_[2]); - for (std::size_t i = 0; i < 3; ++i) - rbufstate_[i] = tmpbufstate[i]; - rsrc_ = src; - } - return rsrc_; - } + void swap(basic_pstream& rhs) { + iostream_type::swap(rhs); + pbase_type::swap(rhs); + } +#endif // C++11 /** - * Suspends execution and waits for the associated process to exit, or - * until a signal is delivered whose action is to terminate the current - * process or to call a signal handling function. If the process has - * already exited (i.e. it is a "zombie" process) then wait() returns - * immediately. Waiting for the child process causes all its system - * resources to be freed. + * @brief Destructor * - * error() will return EINTR if wait() is interrupted by a signal. - * - * @param nohang true to return immediately if the process has not exited. - * @return 1 if the process has exited and wait() has not yet been called. - * 0 if @a nohang is true and the process has not exited yet. - * -1 if no process has been started or if an error occurs, - * in which case the error can be found using error(). + * Closes the stream and waits for the child to exit. */ - template - int - basic_pstreambuf::wait(bool nohang) - { - int child_exited = -1; - if (is_open()) - { - int exit_status; - switch(::waitpid(ppid_, &exit_status, nohang ? WNOHANG : 0)) - { - case 0 : - // nohang was true and process has not exited - child_exited = 0; - break; - case -1 : - error_ = errno; - break; - default : - // process has exited - ppid_ = 0; - status_ = exit_status; - child_exited = 1; - // Close wpipe, would get SIGPIPE if we used it. - destroy_buffers(pstdin); - close_fd(wpipe_); - // Must free read buffers and pipes on destruction - // or next call to open()/close() - break; - } - } - return child_exited; - } + ~basic_pstream() {} /** - * Sends the specified signal to the process. A signal can be used to - * terminate a child process that would not exit otherwise. + * @brief Start a process. * - * If an error occurs the error code will be set to one of the possible - * errors for @c kill(). See your system's documentation for these errors. + * Calls do_open( @a cnd , @a mode ). * - * @param signal A signal to send to the child process. - * @return @c this or @c NULL if @c kill() fails. + * @param cmd a string containing a shell command. + * @param mode the I/O mode to use when opening the pipe. + * @see do_open(const std::string&, pmode) */ - template - inline basic_pstreambuf* - basic_pstreambuf::kill(int signal) - { - basic_pstreambuf* ret = NULL; - if (is_open()) - { - if (::kill(ppid_, signal)) - error_ = errno; - else - { -#if 0 - // TODO call exited() to check for exit and clean up? leave to user? - if (signal==SIGTERM || signal==SIGKILL) - this->exited(); -#endif - ret = this; - } - } - return ret; - } + void open(const std::string& cmd, pmode mode = pstdout | pstdin) { + this->do_open(cmd, mode); + } /** - * Sends the specified signal to the process group of the child process. - * A signal can be used to terminate a child process that would not exit - * otherwise, or to kill the process and its own children. + * @brief Start a process. * - * If an error occurs the error code will be set to one of the possible - * errors for @c getpgid() or @c kill(). See your system's documentation - * for these errors. If the child is in the current process group then - * NULL will be returned and the error code set to EPERM. + * Calls do_open( @a file , @a argv , @a mode ). * - * @param signal A signal to send to the child process. - * @return @c this on success or @c NULL on failure. + * @param file a string containing the pathname of a program to execute. + * @param argv a vector of argument strings passed to the new program. + * @param mode the I/O mode to use when opening the pipe. + * @see do_open(const std::string&, const argv_type&, pmode) */ - template - inline basic_pstreambuf* - basic_pstreambuf::killpg(int signal) - { - basic_pstreambuf* ret = NULL; -#ifdef _POSIX_JOB_CONTROL - if (is_open()) - { - pid_t pgid = ::getpgid(ppid_); - if (pgid == -1) - error_ = errno; - else if (pgid == ::getpgrp()) - error_ = EPERM; // Don't commit suicide - else if (::killpg(pgid, signal)) - error_ = errno; - else - ret = this; - } -#else - error_ = ENOTSUP; -#endif - return ret; - } + void open(const std::string& file, const argv_type& argv, + pmode mode = pstdout | pstdin) { + this->do_open(file, argv, mode); + } /** - * This function can call pstreambuf::wait() and so may change the - * object's state if the child process has already exited. - * - * @return True if the associated process has exited, false otherwise. - * @see basic_pstreambuf::wait() + * @brief Set streambuf to read from process' @c stdout. + * @return @c *this */ - template - inline bool - basic_pstreambuf::exited() - { - return ppid_ == 0 || wait(true)==1; - } - + basic_pstream& out() { + this->buf_.read_err(false); + return *this; + } /** - * @return The exit status of the child process, or -1 if wait() - * has not yet been called to wait for the child to exit. - * @see basic_pstreambuf::wait() + * @brief Set streambuf to read from process' @c stderr. + * @return @c *this */ - template - inline int - basic_pstreambuf::status() const - { - return status_; - } + basic_pstream& err() { + this->buf_.read_err(true); + return *this; + } +}; - /** - * @return The error code of the most recently failed operation, or zero. - */ - template - inline int - basic_pstreambuf::error() const - { - return error_; - } +/** + * @class basic_rpstream + * @brief Class template for Restricted PStreams. + * + * Writing to an rpstream opened with @c pmode @c pstdin writes to the + * standard input of the command. + * It is not possible to read directly from an rpstream object, to use + * an rpstream as in istream you must call either basic_rpstream::out() + * or basic_rpstream::err(). This is to prevent accidental reads from + * the wrong input source. If the rpstream was not opened with @c pmode + * @c pstderr then the class cannot read the process' @c stderr, and + * basic_rpstream::err() will return an istream that reads from the + * process' @c stdout, and vice versa. + * Reading from an rpstream opened with @c pmode @c pstdout and/or + * @c pstderr reads the command's standard output and/or standard error. + * Any of the process' @c stdin, @c stdout or @c stderr that is not + * connected to the pstream (as specified by the @c pmode) + * will be the same as the process that created the pstream object, + * unless altered by the command itself. + */ + +template > +class basic_rpstream : public std::basic_ostream, + private std::basic_istream, + private pstream_common, + virtual public pstreams { + typedef std::basic_ostream ostream_type; + typedef std::basic_istream istream_type; + typedef pstream_common pbase_type; + + using pbase_type::buf_; // declare name in this scope + + public: + /// Type used to specify how to connect to the process. + typedef typename pbase_type::pmode pmode; + + /// Type used to hold the arguments for a command. + typedef typename pbase_type::argv_type argv_type; + + /// Default constructor, creates an uninitialised stream. + basic_rpstream() : ostream_type(NULL), istream_type(NULL), pbase_type() {} /** - * Closes the output pipe, causing the child process to receive the - * end-of-file indicator on subsequent reads from its @c stdin stream. + * @brief Constructor that initialises the stream by starting a process. + * + * Initialises the stream buffer by calling do_open() with the supplied + * arguments. + * + * @param cmd a string containing a shell command. + * @param mode the I/O mode to use when opening the pipe. + * @see do_open(const std::string&, pmode) */ - template - inline void - basic_pstreambuf::peof() - { - sync(); - destroy_buffers(pstdin); - close_fd(wpipe_); - } + explicit basic_rpstream(const std::string& cmd, pmode mode = pstdout | pstdin) + : ostream_type(NULL), istream_type(NULL), pbase_type(cmd, mode) {} /** - * Unlike pstreambuf::exited(), this function will not call wait() and - * so will not change the object's state. This means that once a child - * process is executed successfully this function will continue to - * return true even after the process exits (until wait() is called.) + * @brief Constructor that initialises the stream by starting a process. + * + * Initialises the stream buffer by calling do_open() with the supplied + * arguments. * - * @return true if a previous call to open() succeeded and wait() has - * not been called and determined that the process has exited, - * false otherwise. + * @param file a string containing the pathname of a program to execute. + * @param argv a vector of argument strings passed to the new program. + * @param mode the I/O mode to use when opening the pipe. + * @see do_open(const std::string&, const argv_type&, pmode) */ - template - inline bool - basic_pstreambuf::is_open() const - { - return ppid_ > 0; - } + basic_rpstream(const std::string& file, const argv_type& argv, + pmode mode = pstdout | pstdin) + : ostream_type(NULL), istream_type(NULL), pbase_type(file, argv, mode) {} /** - * Toggle the stream used for reading. If @a readerr is @c true then the - * process' @c stderr output will be used for subsequent extractions, if - * @a readerr is false the the process' stdout will be used. - * @param readerr @c true to read @c stderr, @c false to read @c stdout. - * @return @c true if the requested stream is open and will be used for - * subsequent extractions, @c false otherwise. + * @brief Constructor that initialises the stream by starting a process. + * + * Initialises the stream buffer by calling + * @c do_open(argv[0],argv,mode) + * + * @param argv a vector of argument strings passed to the new program. + * @param mode the I/O mode to use when opening the pipe. + * @see do_open(const std::string&, const argv_type&, pmode) */ - template - inline bool - basic_pstreambuf::read_err(bool readerr) - { - buf_read_src src = readerr ? rsrc_err : rsrc_out; - if (rpipe_[src]>=0) - { - switch_read_buffer(src); - return true; - } - return false; - } + explicit basic_rpstream(const argv_type& argv, pmode mode = pstdout | pstdin) + : ostream_type(NULL), + istream_type(NULL), + pbase_type(argv.at(0), argv, mode) {} +#if __cplusplus >= 201103L /** - * Called when the internal character buffer is not present or is full, - * to transfer the buffer contents to the pipe. + * @brief Constructor that initialises the stream by starting a process. * - * @param c a character to be written to the pipe. - * @return @c traits_type::eof() if an error occurs, otherwise if @a c - * is not equal to @c traits_type::eof() it will be buffered and - * a value other than @c traits_type::eof() returned to indicate - * success. + * @param l a list of argument strings passed to the new program. + * @param mode the I/O mode to use when opening the pipe. + * @see do_open(const std::string&, const argv_type&, pmode) */ - template - typename basic_pstreambuf::int_type - basic_pstreambuf::overflow(int_type c) - { - if (!empty_buffer()) - return traits_type::eof(); - else if (!traits_type::eq_int_type(c, traits_type::eof())) - return this->sputc(c); - else - return traits_type::not_eof(c); - } - + template > + explicit basic_rpstream(std::initializer_list l, + pmode mode = pstdout | pstdin) + : basic_rpstream(argv_type(l.begin(), l.end()), mode) {} - template - int - basic_pstreambuf::sync() - { - return !exited() && empty_buffer() ? 0 : -1; - } + // TODO: figure out how to move istream and ostream bases separately, + // but so the virtual basic_ios base is only modified once. +#if 0 + basic_rpstream(basic_rpstream&& rhs) + : iostream_type(std::move(rhs)) + , pbase_type(std::move(rhs)) + { iostream_type::set_rdbuf(std::addressof(pbase_type::buf_)); } - /** - * @param s character buffer. - * @param n buffer length. - * @return the number of characters written. - */ - template - std::streamsize - basic_pstreambuf::xsputn(const char_type* s, std::streamsize n) - { - std::streamsize done = 0; - while (done < n) + basic_rpstream& + operator=(basic_rpstream&& rhs) { - if (std::streamsize nbuf = this->epptr() - this->pptr()) - { - nbuf = std::min(nbuf, n - done); - traits_type::copy(this->pptr(), s + done, nbuf); - this->pbump(nbuf); - done += nbuf; - } - else if (!empty_buffer()) - break; + iostream_type::operator=(std::move(rhs)); + pbase_type::operator=(std::move(rhs)); + return *this; } - return done; - } - /** - * @return true if the buffer was emptied, false otherwise. - */ - template - bool - basic_pstreambuf::empty_buffer() - { - const std::streamsize count = this->pptr() - this->pbase(); - if (count > 0) + void + swap(basic_rpstream& rhs) { - const std::streamsize written = this->write(this->wbuffer_, count); - if (written > 0) - { - if (const std::streamsize unwritten = count - written) - traits_type::move(this->pbase(), this->pbase()+written, unwritten); - this->pbump(-written); - return true; - } + iostream_type::swap(rhs); + pbase_type::swap(rhs); } - return false; - } +#endif +#endif // C++11 + + /// Destructor + ~basic_rpstream() {} /** - * Called when the internal character buffer is is empty, to re-fill it - * from the pipe. + * @brief Start a process. * - * @return The first available character in the buffer, - * or @c traits_type::eof() in case of failure. + * Calls do_open( @a cmd , @a mode ). + * + * @param cmd a string containing a shell command. + * @param mode the I/O mode to use when opening the pipe. + * @see do_open(const std::string&, pmode) */ - template - typename basic_pstreambuf::int_type - basic_pstreambuf::underflow() - { - if (this->gptr() < this->egptr() || fill_buffer()) - return traits_type::to_int_type(*this->gptr()); - else - return traits_type::eof(); - } + void open(const std::string& cmd, pmode mode = pstdout | pstdin) { + this->do_open(cmd, mode); + } /** - * Attempts to make @a c available as the next character to be read by - * @c sgetc(). + * @brief Start a process. * - * @param c a character to make available for extraction. - * @return @a c if the character can be made available, - * @c traits_type::eof() otherwise. + * Calls do_open( @a file , @a argv , @a mode ). + * + * @param file a string containing the pathname of a program to execute. + * @param argv a vector of argument strings passed to the new program. + * @param mode the I/O mode to use when opening the pipe. + * @see do_open(const std::string&, const argv_type&, pmode) */ - template - typename basic_pstreambuf::int_type - basic_pstreambuf::pbackfail(int_type c) - { - if (this->gptr() != this->eback()) - { - this->gbump(-1); - if (!traits_type::eq_int_type(c, traits_type::eof())) - *this->gptr() = traits_type::to_char_type(c); - return traits_type::not_eof(c); - } - else - return traits_type::eof(); - } + void open(const std::string& file, const argv_type& argv, + pmode mode = pstdout | pstdin) { + this->do_open(file, argv, mode); + } - template - std::streamsize - basic_pstreambuf::showmanyc() - { - int avail = 0; - if (sizeof(char_type) == 1) - avail = fill_buffer(true) ? this->egptr() - this->gptr() : -1; -#ifdef FIONREAD - else - { - if (::ioctl(rpipe(), FIONREAD, &avail) == -1) - avail = -1; - else if (avail) - avail /= sizeof(char_type); - } -#endif - return std::streamsize(avail); - } + /** + * @brief Obtain a reference to the istream that reads + * the process' @c stdout. + * @return @c *this + */ + istream_type& out() { + this->buf_.read_err(false); + return *this; + } /** - * @return true if the buffer was filled, false otherwise. + * @brief Obtain a reference to the istream that reads + * the process' @c stderr. + * @return @c *this */ - template - bool - basic_pstreambuf::fill_buffer(bool non_blocking) - { - const std::streamsize pb1 = this->gptr() - this->eback(); - const std::streamsize pb2 = pbsz; - const std::streamsize npb = std::min(pb1, pb2); + istream_type& err() { + this->buf_.read_err(true); + return *this; + } +}; + +/// Type definition for common template specialisation. +typedef basic_pstreambuf pstreambuf; +/// Type definition for common template specialisation. +typedef basic_ipstream ipstream; +/// Type definition for common template specialisation. +typedef basic_opstream opstream; +/// Type definition for common template specialisation. +typedef basic_pstream pstream; +/// Type definition for common template specialisation. +typedef basic_rpstream rpstream; - char_type* const rbuf = rbuffer(); +/** + * When inserted into an output pstream the manipulator calls + * basic_pstreambuf::peof() to close the output pipe, + * causing the child process to receive the end-of-file indicator + * on subsequent reads from its @c stdin stream. + * + * @brief Manipulator to close the pipe connected to the process' stdin. + * @param s An output PStream class. + * @return The stream object the manipulator was invoked on. + * @see basic_pstreambuf::peof() + * @relates basic_opstream basic_pstream basic_rpstream + */ +template +inline std::basic_ostream& peof(std::basic_ostream& s) { + typedef basic_pstreambuf pstreambuf_type; + if (pstreambuf_type* p = dynamic_cast(s.rdbuf())) p->peof(); + return s; +} + +/* + * member definitions for pstreambuf + */ - if (npb) - traits_type::move(rbuf + pbsz - npb, this->gptr() - npb, npb); +/** + * @class basic_pstreambuf + * Provides underlying streambuf functionality for the PStreams classes. + */ - std::streamsize rc = -1; +/** Creates an uninitialised stream buffer. */ +template +inline basic_pstreambuf::basic_pstreambuf() + : ppid_(-1) // initialise to -1 to indicate no process run yet. + , + wpipe_(-1), + wbuffer_(), + rbuffer_(), + rbufstate_(), + rsrc_(rsrc_out), + status_(-1), + error_(0) { + rpipe_[rsrc_out] = rpipe_[rsrc_err] = -1; +} - if (non_blocking) - { - const int flags = ::fcntl(rpipe(), F_GETFL); - if (flags != -1) - { - const bool blocking = !(flags & O_NONBLOCK); - if (blocking) - ::fcntl(rpipe(), F_SETFL, flags | O_NONBLOCK); // set non-blocking - - error_ = 0; - rc = read(rbuf + pbsz, bufsz - pbsz); - - if (rc == -1 && error_ == EAGAIN) // nothing available - rc = 0; - else if (rc == 0) // EOF - rc = -1; - - if (blocking) - ::fcntl(rpipe(), F_SETFL, flags); // restore - } - } - else - rc = read(rbuf + pbsz, bufsz - pbsz); +/** + * Initialises the stream buffer by calling open() with the supplied + * arguments. + * + * @param cmd a string containing a shell command. + * @param mode the I/O mode to use when opening the pipe. + * @see open() + */ +template +inline basic_pstreambuf::basic_pstreambuf(const std::string& cmd, + pmode mode) + : ppid_(-1) // initialise to -1 to indicate no process run yet. + , + wpipe_(-1), + wbuffer_(), + rbuffer_(), + rbufstate_(), + rsrc_(rsrc_out), + status_(-1), + error_(0) { + rpipe_[rsrc_out] = rpipe_[rsrc_err] = -1; + open(cmd, mode); +} - if (rc > 0 || (rc == 0 && non_blocking)) - { - this->setg( rbuf + pbsz - npb, - rbuf + pbsz, - rbuf + pbsz + rc ); - return true; - } - else - { - this->setg(NULL, NULL, NULL); - return false; - } - } +/** + * Initialises the stream buffer by calling open() with the supplied + * arguments. + * + * @param file a string containing the name of a program to execute. + * @param argv a vector of argument strings passsed to the new program. + * @param mode the I/O mode to use when opening the pipe. + * @see open() + */ +template +inline basic_pstreambuf::basic_pstreambuf(const std::string& file, + const argv_type& argv, + pmode mode) + : ppid_(-1) // initialise to -1 to indicate no process run yet. + , + wpipe_(-1), + wbuffer_(), + rbuffer_(), + rbufstate_(), + rsrc_(rsrc_out), + status_(-1), + error_(0) { + rpipe_[rsrc_out] = rpipe_[rsrc_err] = -1; + open(file, argv, mode); +} - /** - * Writes up to @a n characters to the pipe from the buffer @a s. - * - * @param s character buffer. - * @param n buffer length. - * @return the number of characters written. - */ - template - inline std::streamsize - basic_pstreambuf::write(const char_type* s, std::streamsize n) - { - std::streamsize nwritten = 0; - if (wpipe() >= 0) - { - nwritten = ::write(wpipe(), s, n * sizeof(char_type)); - if (nwritten == -1) - error_ = errno; - else - nwritten /= sizeof(char_type); - } - return nwritten; +/** + * Closes the stream by calling close(). + * @see close() + */ +template +inline basic_pstreambuf::~basic_pstreambuf() { + close(); +} + +#if __cplusplus >= 201103L +/** + * Move constructor. + */ +template +inline basic_pstreambuf::basic_pstreambuf(basic_pstreambuf&& rhs) noexcept + : basic_streambuf(static_cast(rhs)), + ppid_(rhs.ppid_), + wpipe_(rhs.wpipe_), + rpipe_{rhs.rpipe_[0], rhs.rpipe_[1]}, + wbuffer_(rhs.wbuffer_), + rbuffer_{rhs.rbuffer_[0], rhs.rbuffer_[1]}, + rbufstate_{rhs.rbufstate_[0], rhs.rbufstate_[1], rhs.rbufstate_[2]}, + rsrc_(rhs.rsrc_), + status_(rhs.status_), + error_(rhs.error_) { + rhs.ppid_ = -1; + rhs.wpipe_ = -1; + rhs.rpipe_[0] = rhs.rpipe_[1] = -1; + rhs.wbuffer_ = nullptr; + rhs.rbuffer_[0] = rhs.rbuffer_[1] = nullptr; + rhs.rbufstate_[0] = rhs.rbufstate_[1] = rhs.rbufstate_[2] = nullptr; + rhs.rsrc_ = rsrc_out; + rhs.status_ = -1; + rhs.error_ = 0; + rhs.setg(nullptr, nullptr, nullptr); + rhs.setp(nullptr, nullptr); +} + +template +inline basic_pstreambuf& basic_pstreambuf::operator=( + basic_pstreambuf&& rhs) noexcept { + close(); + basic_streambuf::operator=(static_cast(rhs)); + swap(rhs); + return *this; +} + +template +inline void basic_pstreambuf::swap(basic_pstreambuf& rhs) noexcept { + basic_streambuf::swap(static_cast(rhs)); + std::swap(ppid_, rhs.ppid_); + std::swap(wpipe_, rhs.wpipe_); + std::swap(rpipe_, rhs.rpipe_); + std::swap(wbuffer_, rhs.wbuffer_); + std::swap(rbuffer_, rhs.rbuffer_); + std::swap(rbufstate_, rhs.rbufstate_); + std::swap(rsrc_, rhs.rsrc_); + std::swap(status_, rhs.status_); + std::swap(error_, rhs.error_); +} +#endif // C++11 + +/** + * Starts a new process by passing @a command to the shell (/bin/sh) + * and opens pipes to the process with the specified @a mode. + * + * If @a mode contains @c pstdout the initial read source will be + * the child process' stdout, otherwise if @a mode contains @c pstderr + * the initial read source will be the child's stderr. + * + * Will duplicate the actions of the shell in searching for an + * executable file if the specified file name does not contain a slash (/) + * character. + * + * @warning + * There is no way to tell whether the shell command succeeded, this + * function will always succeed unless resource limits (such as + * memory usage, or number of processes or open files) are exceeded. + * This means is_open() will return true even if @a command cannot + * be executed. + * Use pstreambuf::open(const std::string&, const argv_type&, pmode) + * if you need to know whether the command failed to execute. + * + * @param command a string containing a shell command. + * @param mode a bitwise OR of one or more of @c out, @c in, @c err. + * @return NULL if the shell could not be started or the + * pipes could not be opened, @c this otherwise. + * @see execl(3) + */ +template +basic_pstreambuf* basic_pstreambuf::open(const std::string& command, + pmode mode) { + const char* shell_path = "/bin/sh"; +#if 0 + const std::string argv[] = { "sh", "-c", command }; + return this->open(shell_path, argv_type(argv, argv+3), mode); +#else + basic_pstreambuf* ret = NULL; + + if (!is_open()) { + switch (fork(mode)) { + case 0: + // this is the new process, exec command + ::execl(shell_path, "sh", "-c", command.c_str(), (char*)NULL); + + // can only reach this point if exec() failed + + // parent can get exit code from waitpid() + ::_exit(errno); + // using std::exit() would make static dtors run twice + + case -1: + // couldn't fork, error already handled in pstreambuf::fork() + break; + + default: + // this is the parent process + // activate buffers + create_buffers(mode); + ret = this; } + } + return ret; +#endif +} - /** - * Reads up to @a n characters from the pipe to the buffer @a s. - * - * @param s character buffer. - * @param n buffer length. - * @return the number of characters read. - */ - template - inline std::streamsize - basic_pstreambuf::read(char_type* s, std::streamsize n) - { - std::streamsize nread = 0; - if (rpipe() >= 0) - { - nread = ::read(rpipe(), s, n * sizeof(char_type)); - if (nread == -1) - error_ = errno; - else - nread /= sizeof(char_type); +/** + * @brief Helper function to close a file descriptor. + * + * Inspects @a fd and calls close(3) if it has a non-negative value. + * + * @param fd a file descriptor. + * @relates basic_pstreambuf + */ +inline void close_fd(pstreams::fd_type& fd) { + if (fd >= 0 && ::close(fd) == 0) fd = -1; +} + +/** + * @brief Helper function to close an array of file descriptors. + * + * Calls @c close_fd() on each member of the array. + * The length of the array is determined automatically by + * template argument deduction to avoid errors. + * + * @param fds an array of file descriptors. + * @relates basic_pstreambuf + */ +template +inline void close_fd_array(pstreams::fd_type (&fds)[N]) { + for (std::size_t i = 0; i < N; ++i) close_fd(fds[i]); +} + +/** + * Starts a new process by executing @a file with the arguments in + * @a argv and opens pipes to the process with the specified @a mode. + * + * By convention @c argv[0] should be the file name of the file being + * executed. + * + * If @a mode contains @c pstdout the initial read source will be + * the child process' stdout, otherwise if @a mode contains @c pstderr + * the initial read source will be the child's stderr. + * + * Will duplicate the actions of the shell in searching for an + * executable file if the specified file name does not contain a slash (/) + * character. + * + * Iff @a file is successfully executed then is_open() will return true. + * Otherwise, pstreambuf::error() can be used to obtain the value of + * @c errno that was set by execvp(3) in the child process. + * + * The exit status of the new process will be returned by + * pstreambuf::status() after pstreambuf::exited() returns true. + * + * @param file a string containing the pathname of a program to execute. + * @param argv a vector of argument strings passed to the new program. + * @param mode a bitwise OR of one or more of @c out, @c in and @c err. + * @return NULL if a pipe could not be opened or if the program could + * not be executed, @c this otherwise. + * @see execvp(3) + */ +template +basic_pstreambuf* basic_pstreambuf::open(const std::string& file, + const argv_type& argv, + pmode mode) { + basic_pstreambuf* ret = NULL; + + if (!is_open()) { + // constants for read/write ends of pipe + enum { RD, WR }; + + // open another pipe and set close-on-exec + fd_type ck_exec[] = {-1, -1}; + if (-1 == ::pipe(ck_exec) || + -1 == ::fcntl(ck_exec[RD], F_SETFD, FD_CLOEXEC) || + -1 == ::fcntl(ck_exec[WR], F_SETFD, FD_CLOEXEC)) { + error_ = errno; + close_fd_array(ck_exec); + } else { + switch (fork(mode)) { + case 0: + // this is the new process, exec command + { + char** arg_v = new char*[argv.size() + 1]; + for (std::size_t i = 0; i < argv.size(); ++i) { + const std::string& src = argv[i]; + char*& dest = arg_v[i]; + dest = new char[src.size() + 1]; + dest[src.copy(dest, src.size())] = '\0'; + } + arg_v[argv.size()] = NULL; + + ::execvp(file.c_str(), arg_v); + + // can only reach this point if exec() failed + + // parent can get error code from ck_exec pipe + error_ = errno; + + while (::write(ck_exec[WR], &error_, sizeof(error_)) == -1 && + errno == EINTR) { + } + + ::close(ck_exec[WR]); + ::close(ck_exec[RD]); + + ::_exit(error_); + // using std::exit() would make static dtors run twice + } + + case -1: + // couldn't fork, error already handled in pstreambuf::fork() + close_fd_array(ck_exec); + break; + + default: + // this is the parent process + + // check child called exec() successfully + ::close(ck_exec[WR]); + switch (::read(ck_exec[RD], &error_, sizeof(error_))) { + case 0: + // activate buffers + create_buffers(mode); + ret = this; + break; + case -1: + error_ = errno; + break; + default: + // error_ contains error code from child + // call wait() to clean up and set ppid_ to 0 + this->wait(); + break; + } + ::close(ck_exec[RD]); } - return nread; } + } + return ret; +} - /** @return a reference to the output file descriptor */ - template - inline pstreams::fd_type& - basic_pstreambuf::wpipe() - { - return wpipe_; - } +/** + * Gets the PID + * @return pid_t + */ +template +pid_t basic_pstreambuf::getpid() { + return ppid_; +} - /** @return a reference to the active input file descriptor */ - template - inline pstreams::fd_type& - basic_pstreambuf::rpipe() - { - return rpipe_[rsrc_]; - } +/** + * Creates pipes as specified by @a mode and calls @c fork() to create + * a new process. If the fork is successful the parent process stores + * the child's PID and the opened pipes and the child process replaces + * its standard streams with the opened pipes. + * + * If an error occurs the error code will be set to one of the possible + * errors for @c pipe() or @c fork(). + * See your system's documentation for these error codes. + * + * @param mode an OR of pmodes specifying which of the child's + * standard streams to connect to. + * @return On success the PID of the child is returned in the parent's + * context and zero is returned in the child's context. + * On error -1 is returned and the error code is set appropriately. + */ +template +pid_t basic_pstreambuf::fork(pmode mode) { + pid_t pid = -1; - /** @return a reference to the specified input file descriptor */ - template - inline pstreams::fd_type& - basic_pstreambuf::rpipe(buf_read_src which) - { - return rpipe_[which]; - } + // Three pairs of file descriptors, for pipes connected to the + // process' stdin, stdout and stderr + // (stored in a single array so close_fd_array() can close all at once) + fd_type fd[] = {-1, -1, -1, -1, -1, -1}; + fd_type* const pin = fd; + fd_type* const pout = fd + 2; + fd_type* const perr = fd + 4; - /** @return a pointer to the start of the active input buffer area. */ - template - inline typename basic_pstreambuf::char_type* - basic_pstreambuf::rbuffer() - { - return rbuffer_[rsrc_]; - } + // constants for read/write ends of pipe + enum { RD, WR }; + // N.B. + // For the pstreambuf pin is an output stream and + // pout and perr are input streams. - /* - * member definitions for pstream_common - */ + if (!error_ && mode & pstdin && ::pipe(pin)) error_ = errno; - /** - * @class pstream_common - * Abstract Base Class providing common functionality for basic_ipstream, - * basic_opstream and basic_pstream. - * pstream_common manages the basic_pstreambuf stream buffer that is used - * by the derived classes to initialise an iostream class. - */ + if (!error_ && mode & pstdout && ::pipe(pout)) error_ = errno; - /** Creates an uninitialised stream. */ - template - inline - pstream_common::pstream_common() - : std::basic_ios(NULL) - , command_() - , buf_() - { - this->std::basic_ios::rdbuf(&buf_); - } + if (!error_ && mode & pstderr && ::pipe(perr)) error_ = errno; - /** - * Initialises the stream buffer by calling - * do_open( @a command , @a mode ) - * - * @param cmd a string containing a shell command. - * @param mode the I/O mode to use when opening the pipe. - * @see do_open(const std::string&, pmode) - */ - template - inline - pstream_common::pstream_common(const std::string& cmd, pmode mode) - : std::basic_ios(NULL) - , command_(cmd) - , buf_() - { - this->std::basic_ios::rdbuf(&buf_); - do_open(cmd, mode); - } + if (!error_) { + pid = ::fork(); + switch (pid) { + case 0: { + // this is the new process - /** - * Initialises the stream buffer by calling - * do_open( @a file , @a argv , @a mode ) - * - * @param file a string containing the pathname of a program to execute. - * @param argv a vector of argument strings passed to the new program. - * @param mode the I/O mode to use when opening the pipe. - * @see do_open(const std::string&, const argv_type&, pmode) - */ - template - inline - pstream_common::pstream_common( const std::string& file, - const argv_type& argv, - pmode mode ) - : std::basic_ios(NULL) - , command_(file) - , buf_() - { - this->std::basic_ios::rdbuf(&buf_); - do_open(file, argv, mode); - } + // for each open pipe close one end and redirect the + // respective standard stream to the other end - /** - * This is a pure virtual function to make @c pstream_common abstract. - * Because it is the destructor it will be called by derived classes - * and so must be defined. It is also protected, to discourage use of - * the PStreams classes through pointers or references to the base class. - * - * @sa If defining a pure virtual seems odd you should read - * http://www.gotw.ca/gotw/031.htm (and the rest of the site as well!) - */ - template - inline - pstream_common::~pstream_common() - { - } + if (*pin >= 0) { + ::close(pin[WR]); + ::dup2(pin[RD], STDIN_FILENO); + ::close(pin[RD]); + } + if (*pout >= 0) { + ::close(pout[RD]); + ::dup2(pout[WR], STDOUT_FILENO); + ::close(pout[WR]); + } + if (*perr >= 0) { + ::close(perr[RD]); + ::dup2(perr[WR], STDERR_FILENO); + ::close(perr[WR]); + } - /** - * Gets the PID - * @return pid_t - */ - template - inline pid_t - pstream_common::getpid() - { - return buf_.getpid(); - } - - /** - * Calls rdbuf()->open( @a command , @a mode ) - * and sets @c failbit on error. - * - * @param cmd a string containing a shell command. - * @param mode the I/O mode to use when opening the pipe. - * @see basic_pstreambuf::open(const std::string&, pmode) - */ - template - inline void - pstream_common::do_open(const std::string& cmd, pmode mode) - { - if (!buf_.open((command_=cmd), mode)) - this->setstate(std::ios_base::failbit); +#ifdef _POSIX_JOB_CONTROL + if (mode & newpg) ::setpgid(0, 0); // Change to a new process group +#endif + + break; + } + case -1: { + // couldn't fork for some reason + error_ = errno; + // close any open pipes + close_fd_array(fd); + break; + } + default: { + // this is the parent process, store process' pid + ppid_ = pid; + + // store one end of open pipes and close other end + if (*pin >= 0) { + wpipe_ = pin[WR]; + ::close(pin[RD]); + } + if (*pout >= 0) { + rpipe_[rsrc_out] = pout[RD]; + ::close(pout[WR]); + } + if (*perr >= 0) { + rpipe_[rsrc_err] = perr[RD]; + ::close(perr[WR]); + } + } } + } else { + // close any pipes we opened before failure + close_fd_array(fd); + } + return pid; +} - /** - * Calls rdbuf()->open( @a file, @a argv, @a mode ) - * and sets @c failbit on error. - * - * @param file a string containing the pathname of a program to execute. - * @param argv a vector of argument strings passed to the new program. - * @param mode the I/O mode to use when opening the pipe. - * @see basic_pstreambuf::open(const std::string&, const argv_type&, pmode) - */ - template - inline void - pstream_common::do_open( const std::string& file, - const argv_type& argv, - pmode mode ) - { - if (!buf_.open((command_=file), argv, mode)) - this->setstate(std::ios_base::failbit); +/** + * Closes all pipes and calls wait() to wait for the process to finish. + * If an error occurs the error code will be set to one of the possible + * errors for @c waitpid(). + * See your system's documentation for these errors. + * + * @return @c this on successful close or @c NULL if there is no + * process to close or if an error occurs. + */ +template +basic_pstreambuf* basic_pstreambuf::close() { + const bool running = is_open(); + + basic_pstreambuf::sync(); // might call wait() and reap the child process + + // rather than trying to work out whether or not we need to clean up + // just do it anyway, all cleanup functions are safe to call twice. + + destroy_buffers(pstdin | pstdout | pstderr); + + // close pipes before wait() so child gets EOF/SIGPIPE + close_fd(wpipe_); + close_fd_array(rpipe_); + + do { + error_ = 0; + } while (wait() == -1 && error() == EINTR); + + return running ? this : NULL; +} + +/** + * Used to be called on construction to initialise the arrays for reading. + * No longer used. + */ +template +#if __cplusplus >= 201402L && __has_cpp_attribute(deprecated) +[[deprecated]] +#elif __GNUC__ +__attribute__((deprecated)) +#endif +inline void +basic_pstreambuf::init_rbuffers() { + rpipe_[rsrc_out] = rpipe_[rsrc_err] = -1; + rbuffer_[rsrc_out] = rbuffer_[rsrc_err] = NULL; + rbufstate_[0] = rbufstate_[1] = rbufstate_[2] = NULL; +} + +template +void basic_pstreambuf::create_buffers(pmode mode) { + if (mode & pstdin) { + delete[] wbuffer_; + wbuffer_ = new char_type[bufsz]; + this->setp(wbuffer_, wbuffer_ + bufsz); + } + if (mode & pstdout) { + delete[] rbuffer_[rsrc_out]; + rbuffer_[rsrc_out] = new char_type[bufsz]; + rsrc_ = rsrc_out; + this->setg(rbuffer_[rsrc_out] + pbsz, rbuffer_[rsrc_out] + pbsz, + rbuffer_[rsrc_out] + pbsz); + } + if (mode & pstderr) { + delete[] rbuffer_[rsrc_err]; + rbuffer_[rsrc_err] = new char_type[bufsz]; + if (!(mode & pstdout)) { + rsrc_ = rsrc_err; + this->setg(rbuffer_[rsrc_err] + pbsz, rbuffer_[rsrc_err] + pbsz, + rbuffer_[rsrc_err] + pbsz); } + } +} + +template +void basic_pstreambuf::destroy_buffers(pmode mode) { + if (mode & pstdin) { + this->setp(NULL, NULL); + delete[] wbuffer_; + wbuffer_ = NULL; + } + if (mode & pstdout) { + if (rsrc_ == rsrc_out) this->setg(NULL, NULL, NULL); + delete[] rbuffer_[rsrc_out]; + rbuffer_[rsrc_out] = NULL; + } + if (mode & pstderr) { + if (rsrc_ == rsrc_err) this->setg(NULL, NULL, NULL); + delete[] rbuffer_[rsrc_err]; + rbuffer_[rsrc_err] = NULL; + } +} + +template +typename basic_pstreambuf::buf_read_src +basic_pstreambuf::switch_read_buffer(buf_read_src src) { + if (rsrc_ != src) { + char_type* tmpbufstate[] = {this->eback(), this->gptr(), this->egptr()}; + this->setg(rbufstate_[0], rbufstate_[1], rbufstate_[2]); + for (std::size_t i = 0; i < 3; ++i) rbufstate_[i] = tmpbufstate[i]; + rsrc_ = src; + } + return rsrc_; +} - /** Calls rdbuf->close() and sets @c failbit on error. Returns - * process's exit status, as pclose(3) does. */ - template - inline int - pstream_common::close() - { - if (!buf_.close()) - this->setstate(std::ios_base::failbit); - return buf_.status(); +/** + * Suspends execution and waits for the associated process to exit, or + * until a signal is delivered whose action is to terminate the current + * process or to call a signal handling function. If the process has + * already exited (i.e. it is a "zombie" process) then wait() returns + * immediately. Waiting for the child process causes all its system + * resources to be freed. + * + * error() will return EINTR if wait() is interrupted by a signal. + * + * @param nohang true to return immediately if the process has not exited. + * @return 1 if the process has exited and wait() has not yet been called. + * 0 if @a nohang is true and the process has not exited yet. + * -1 if no process has been started or if an error occurs, + * in which case the error can be found using error(). + */ +template +int basic_pstreambuf::wait(bool nohang) { + int child_exited = -1; + if (is_open()) { + int exit_status; + switch (::waitpid(ppid_, &exit_status, nohang ? WNOHANG : 0)) { + case 0: + // nohang was true and process has not exited + child_exited = 0; + break; + case -1: + error_ = errno; + break; + default: + // process has exited + ppid_ = 0; + status_ = exit_status; + child_exited = 1; + // Close wpipe, would get SIGPIPE if we used it. + destroy_buffers(pstdin); + close_fd(wpipe_); + // Must free read buffers and pipes on destruction + // or next call to open()/close() + break; } + } + return child_exited; +} - /** - * @return rdbuf()->is_open(). - * @see basic_pstreambuf::is_open() - */ - template - inline bool - pstream_common::is_open() const - { - return buf_.is_open(); +/** + * Sends the specified signal to the process. A signal can be used to + * terminate a child process that would not exit otherwise. + * + * If an error occurs the error code will be set to one of the possible + * errors for @c kill(). See your system's documentation for these errors. + * + * @param signal A signal to send to the child process. + * @return @c this or @c NULL if @c kill() fails. + */ +template +inline basic_pstreambuf* basic_pstreambuf::kill(int signal) { + basic_pstreambuf* ret = NULL; + if (is_open()) { + if (::kill(ppid_, signal)) + error_ = errno; + else { +#if 0 + // TODO call exited() to check for exit and clean up? leave to user? + if (signal==SIGTERM || signal==SIGKILL) + this->exited(); +#endif + ret = this; } + } + return ret; +} + +/** + * Sends the specified signal to the process group of the child process. + * A signal can be used to terminate a child process that would not exit + * otherwise, or to kill the process and its own children. + * + * If an error occurs the error code will be set to one of the possible + * errors for @c getpgid() or @c kill(). See your system's documentation + * for these errors. If the child is in the current process group then + * NULL will be returned and the error code set to EPERM. + * + * @param signal A signal to send to the child process. + * @return @c this on success or @c NULL on failure. + */ +template +inline basic_pstreambuf* basic_pstreambuf::killpg(int signal) { + basic_pstreambuf* ret = NULL; +#ifdef _POSIX_JOB_CONTROL + if (is_open()) { + pid_t pgid = ::getpgid(ppid_); + if (pgid == -1) + error_ = errno; + else if (pgid == ::getpgrp()) + error_ = EPERM; // Don't commit suicide + else if (::killpg(pgid, signal)) + error_ = errno; + else + ret = this; + } +#else + error_ = ENOTSUP; +#endif + return ret; +} + +/** + * This function can call pstreambuf::wait() and so may change the + * object's state if the child process has already exited. + * + * @return True if the associated process has exited, false otherwise. + * @see basic_pstreambuf::wait() + */ +template +inline bool basic_pstreambuf::exited() { + return ppid_ == 0 || wait(true) == 1; +} + +/** + * @return The exit status of the child process, or -1 if wait() + * has not yet been called to wait for the child to exit. + * @see basic_pstreambuf::wait() + */ +template +inline int basic_pstreambuf::status() const { + return status_; +} + +/** + * @return The error code of the most recently failed operation, or zero. + */ +template +inline int basic_pstreambuf::error() const { + return error_; +} + +/** + * Closes the output pipe, causing the child process to receive the + * end-of-file indicator on subsequent reads from its @c stdin stream. + */ +template +inline void basic_pstreambuf::peof() { + sync(); + destroy_buffers(pstdin); + close_fd(wpipe_); +} + +/** + * Unlike pstreambuf::exited(), this function will not call wait() and + * so will not change the object's state. This means that once a child + * process is executed successfully this function will continue to + * return true even after the process exits (until wait() is called.) + * + * @return true if a previous call to open() succeeded and wait() has + * not been called and determined that the process has exited, + * false otherwise. + */ +template +inline bool basic_pstreambuf::is_open() const { + return ppid_ > 0; +} - /** @return a string containing the command used to initialise the stream. */ - template - inline const std::string& - pstream_common::command() const - { - return command_; +/** + * Toggle the stream used for reading. If @a readerr is @c true then the + * process' @c stderr output will be used for subsequent extractions, if + * @a readerr is false the the process' stdout will be used. + * @param readerr @c true to read @c stderr, @c false to read @c stdout. + * @return @c true if the requested stream is open and will be used for + * subsequent extractions, @c false otherwise. + */ +template +inline bool basic_pstreambuf::read_err(bool readerr) { + buf_read_src src = readerr ? rsrc_err : rsrc_out; + if (rpipe_[src] >= 0) { + switch_read_buffer(src); + return true; + } + return false; +} + +/** + * Called when the internal character buffer is not present or is full, + * to transfer the buffer contents to the pipe. + * + * @param c a character to be written to the pipe. + * @return @c traits_type::eof() if an error occurs, otherwise if @a c + * is not equal to @c traits_type::eof() it will be buffered and + * a value other than @c traits_type::eof() returned to indicate + * success. + */ +template +typename basic_pstreambuf::int_type basic_pstreambuf::overflow( + int_type c) { + if (!empty_buffer()) + return traits_type::eof(); + else if (!traits_type::eq_int_type(c, traits_type::eof())) + return this->sputc(c); + else + return traits_type::not_eof(c); +} + +template +int basic_pstreambuf::sync() { + return !exited() && empty_buffer() ? 0 : -1; +} + +/** + * @param s character buffer. + * @param n buffer length. + * @return the number of characters written. + */ +template +std::streamsize basic_pstreambuf::xsputn(const char_type* s, + std::streamsize n) { + std::streamsize done = 0; + while (done < n) { + if (std::streamsize nbuf = this->epptr() - this->pptr()) { + nbuf = std::min(nbuf, n - done); + traits_type::copy(this->pptr(), s + done, nbuf); + this->pbump(nbuf); + done += nbuf; + } else if (!empty_buffer()) + break; + } + return done; +} + +/** + * @return true if the buffer was emptied, false otherwise. + */ +template +bool basic_pstreambuf::empty_buffer() { + const std::streamsize count = this->pptr() - this->pbase(); + if (count > 0) { + const std::streamsize written = this->write(this->wbuffer_, count); + if (written > 0) { + if (const std::streamsize unwritten = count - written) + traits_type::move(this->pbase(), this->pbase() + written, unwritten); + this->pbump(-written); + return true; } + } + return false; +} + +/** + * Called when the internal character buffer is is empty, to re-fill it + * from the pipe. + * + * @return The first available character in the buffer, + * or @c traits_type::eof() in case of failure. + */ +template +typename basic_pstreambuf::int_type basic_pstreambuf::underflow() { + if (this->gptr() < this->egptr() || fill_buffer()) + return traits_type::to_int_type(*this->gptr()); + else + return traits_type::eof(); +} + +/** + * Attempts to make @a c available as the next character to be read by + * @c sgetc(). + * + * @param c a character to make available for extraction. + * @return @a c if the character can be made available, + * @c traits_type::eof() otherwise. + */ +template +typename basic_pstreambuf::int_type basic_pstreambuf::pbackfail( + int_type c) { + if (this->gptr() != this->eback()) { + this->gbump(-1); + if (!traits_type::eq_int_type(c, traits_type::eof())) + *this->gptr() = traits_type::to_char_type(c); + return traits_type::not_eof(c); + } else + return traits_type::eof(); +} + +template +std::streamsize basic_pstreambuf::showmanyc() { + int avail = 0; + if (sizeof(char_type) == 1) + avail = fill_buffer(true) ? this->egptr() - this->gptr() : -1; +#ifdef FIONREAD + else { + if (::ioctl(rpipe(), FIONREAD, &avail) == -1) + avail = -1; + else if (avail) + avail /= sizeof(char_type); + } +#endif + return std::streamsize(avail); +} + +/** + * @return true if the buffer was filled, false otherwise. + */ +template +bool basic_pstreambuf::fill_buffer(bool non_blocking) { + const std::streamsize pb1 = this->gptr() - this->eback(); + const std::streamsize pb2 = pbsz; + const std::streamsize npb = std::min(pb1, pb2); + + char_type* const rbuf = rbuffer(); - /** @return a pointer to the private stream buffer member. */ - // TODO document behaviour if buffer replaced. - template - inline typename pstream_common::streambuf_type* - pstream_common::rdbuf() const - { - return const_cast(&buf_); + if (npb) traits_type::move(rbuf + pbsz - npb, this->gptr() - npb, npb); + + std::streamsize rc = -1; + + if (non_blocking) { + const int flags = ::fcntl(rpipe(), F_GETFL); + if (flags != -1) { + const bool blocking = !(flags & O_NONBLOCK); + if (blocking) + ::fcntl(rpipe(), F_SETFL, flags | O_NONBLOCK); // set non-blocking + + error_ = 0; + rc = read(rbuf + pbsz, bufsz - pbsz); + + if (rc == -1 && error_ == EAGAIN) // nothing available + rc = 0; + else if (rc == 0) // EOF + rc = -1; + + if (blocking) ::fcntl(rpipe(), F_SETFL, flags); // restore } + } else + rc = read(rbuf + pbsz, bufsz - pbsz); + + if (rc > 0 || (rc == 0 && non_blocking)) { + this->setg(rbuf + pbsz - npb, rbuf + pbsz, rbuf + pbsz + rc); + return true; + } else { + this->setg(NULL, NULL, NULL); + return false; + } +} +/** + * Writes up to @a n characters to the pipe from the buffer @a s. + * + * @param s character buffer. + * @param n buffer length. + * @return the number of characters written. + */ +template +inline std::streamsize basic_pstreambuf::write(const char_type* s, + std::streamsize n) { + std::streamsize nwritten = 0; + if (wpipe() >= 0) { + nwritten = ::write(wpipe(), s, n * sizeof(char_type)); + if (nwritten == -1) + error_ = errno; + else + nwritten /= sizeof(char_type); + } + return nwritten; +} + +/** + * Reads up to @a n characters from the pipe to the buffer @a s. + * + * @param s character buffer. + * @param n buffer length. + * @return the number of characters read. + */ +template +inline std::streamsize basic_pstreambuf::read(char_type* s, + std::streamsize n) { + std::streamsize nread = 0; + if (rpipe() >= 0) { + nread = ::read(rpipe(), s, n * sizeof(char_type)); + if (nread == -1) + error_ = errno; + else + nread /= sizeof(char_type); + } + return nread; +} + +/** @return a reference to the output file descriptor */ +template +inline pstreams::fd_type& basic_pstreambuf::wpipe() { + return wpipe_; +} + +/** @return a reference to the active input file descriptor */ +template +inline pstreams::fd_type& basic_pstreambuf::rpipe() { + return rpipe_[rsrc_]; +} + +/** @return a reference to the specified input file descriptor */ +template +inline pstreams::fd_type& basic_pstreambuf::rpipe(buf_read_src which) { + return rpipe_[which]; +} + +/** @return a pointer to the start of the active input buffer area. */ +template +inline typename basic_pstreambuf::char_type* +basic_pstreambuf::rbuffer() { + return rbuffer_[rsrc_]; +} + +/* + * member definitions for pstream_common + */ + +/** + * @class pstream_common + * Abstract Base Class providing common functionality for basic_ipstream, + * basic_opstream and basic_pstream. + * pstream_common manages the basic_pstreambuf stream buffer that is used + * by the derived classes to initialise an iostream class. + */ + +/** Creates an uninitialised stream. */ +template +inline pstream_common::pstream_common() + : std::basic_ios(NULL), command_(), buf_() { + this->std::basic_ios::rdbuf(&buf_); +} + +/** + * Initialises the stream buffer by calling + * do_open( @a command , @a mode ) + * + * @param cmd a string containing a shell command. + * @param mode the I/O mode to use when opening the pipe. + * @see do_open(const std::string&, pmode) + */ +template +inline pstream_common::pstream_common(const std::string& cmd, pmode mode) + : std::basic_ios(NULL), command_(cmd), buf_() { + this->std::basic_ios::rdbuf(&buf_); + do_open(cmd, mode); +} + +/** + * Initialises the stream buffer by calling + * do_open( @a file , @a argv , @a mode ) + * + * @param file a string containing the pathname of a program to execute. + * @param argv a vector of argument strings passed to the new program. + * @param mode the I/O mode to use when opening the pipe. + * @see do_open(const std::string&, const argv_type&, pmode) + */ +template +inline pstream_common::pstream_common(const std::string& file, + const argv_type& argv, pmode mode) + : std::basic_ios(NULL), command_(file), buf_() { + this->std::basic_ios::rdbuf(&buf_); + do_open(file, argv, mode); +} + +/** + * This is a pure virtual function to make @c pstream_common abstract. + * Because it is the destructor it will be called by derived classes + * and so must be defined. It is also protected, to discourage use of + * the PStreams classes through pointers or references to the base class. + * + * @sa If defining a pure virtual seems odd you should read + * http://www.gotw.ca/gotw/031.htm (and the rest of the site as well!) + */ +template +inline pstream_common::~pstream_common() {} + +/** + * Gets the PID + * @return pid_t + */ +template +inline pid_t pstream_common::getpid() { + return buf_.getpid(); +} + +/** + * Calls rdbuf()->open( @a command , @a mode ) + * and sets @c failbit on error. + * + * @param cmd a string containing a shell command. + * @param mode the I/O mode to use when opening the pipe. + * @see basic_pstreambuf::open(const std::string&, pmode) + */ +template +inline void pstream_common::do_open(const std::string& cmd, pmode mode) { + if (!buf_.open((command_ = cmd), mode)) + this->setstate(std::ios_base::failbit); +} + +/** + * Calls rdbuf()->open( @a file, @a argv, @a mode ) + * and sets @c failbit on error. + * + * @param file a string containing the pathname of a program to execute. + * @param argv a vector of argument strings passed to the new program. + * @param mode the I/O mode to use when opening the pipe. + * @see basic_pstreambuf::open(const std::string&, const argv_type&, pmode) + */ +template +inline void pstream_common::do_open(const std::string& file, + const argv_type& argv, pmode mode) { + if (!buf_.open((command_ = file), argv, mode)) + this->setstate(std::ios_base::failbit); +} + +/** Calls rdbuf->close() and sets @c failbit on error. Returns + * process's exit status, as pclose(3) does. */ +template +inline int pstream_common::close() { + if (!buf_.close()) this->setstate(std::ios_base::failbit); + return buf_.status(); +} + +/** + * @return rdbuf()->is_open(). + * @see basic_pstreambuf::is_open() + */ +template +inline bool pstream_common::is_open() const { + return buf_.is_open(); +} + +/** @return a string containing the command used to initialise the stream. */ +template +inline const std::string& pstream_common::command() const { + return command_; +} + +/** @return a pointer to the private stream buffer member. */ +// TODO document behaviour if buffer replaced. +template +inline typename pstream_common::streambuf_type* +pstream_common::rdbuf() const { + return const_cast(&buf_); +} #if REDI_EVISCERATE_PSTREAMS - /** - * @def REDI_EVISCERATE_PSTREAMS - * If this macro has a non-zero value then certain internals of the - * @c basic_pstreambuf template class are exposed. In general this is - * a Bad Thing, as the internal implementation is largely undocumented - * and may be subject to change at any time, so this feature is only - * provided because it might make PStreams useful in situations where - * it is necessary to do Bad Things. - */ +/** + * @def REDI_EVISCERATE_PSTREAMS + * If this macro has a non-zero value then certain internals of the + * @c basic_pstreambuf template class are exposed. In general this is + * a Bad Thing, as the internal implementation is largely undocumented + * and may be subject to change at any time, so this feature is only + * provided because it might make PStreams useful in situations where + * it is necessary to do Bad Things. + */ - /** - * @warning This function exposes the internals of the stream buffer and - * should be used with caution. It is the caller's responsibility - * to flush streams etc. in order to clear any buffered data. - * The POSIX.1 function fdopen(3) is used to obtain the - * @c FILE pointers from the streambuf's private file descriptor - * members so consult your system's documentation for - * fdopen(3). - * - * @param in A FILE* that will refer to the process' stdin. - * @param out A FILE* that will refer to the process' stdout. - * @param err A FILE* that will refer to the process' stderr. - * @return An OR of zero or more of @c pstdin, @c pstdout, @c pstderr. - * - * For each open stream shared with the child process a @c FILE* is - * obtained and assigned to the corresponding parameter. For closed - * streams @c NULL is assigned to the parameter. - * The return value can be tested to see which parameters should be - * @c !NULL by masking with the corresponding @c pmode value. - * - * @see fdopen(3) - */ - template - std::size_t - basic_pstreambuf::fopen(FILE*& in, FILE*& out, FILE*& err) - { - in = out = err = NULL; - std::size_t open_files = 0; - if (wpipe() > -1) - { - if ((in = ::fdopen(wpipe(), "w"))) - { - open_files |= pstdin; - } - } - if (rpipe(rsrc_out) > -1) - { - if ((out = ::fdopen(rpipe(rsrc_out), "r"))) - { - open_files |= pstdout; - } - } - if (rpipe(rsrc_err) > -1) - { - if ((err = ::fdopen(rpipe(rsrc_err), "r"))) - { - open_files |= pstderr; - } - } - return open_files; +/** + * @warning This function exposes the internals of the stream buffer and + * should be used with caution. It is the caller's responsibility + * to flush streams etc. in order to clear any buffered data. + * The POSIX.1 function fdopen(3) is used to obtain the + * @c FILE pointers from the streambuf's private file descriptor + * members so consult your system's documentation for + * fdopen(3). + * + * @param in A FILE* that will refer to the process' stdin. + * @param out A FILE* that will refer to the process' stdout. + * @param err A FILE* that will refer to the process' stderr. + * @return An OR of zero or more of @c pstdin, @c pstdout, @c pstderr. + * + * For each open stream shared with the child process a @c FILE* is + * obtained and assigned to the corresponding parameter. For closed + * streams @c NULL is assigned to the parameter. + * The return value can be tested to see which parameters should be + * @c !NULL by masking with the corresponding @c pmode value. + * + * @see fdopen(3) + */ +template +std::size_t basic_pstreambuf::fopen(FILE*& in, FILE*& out, FILE*& err) { + in = out = err = NULL; + std::size_t open_files = 0; + if (wpipe() > -1) { + if ((in = ::fdopen(wpipe(), "w"))) { + open_files |= pstdin; } - - /** - * @warning This function exposes the internals of the stream buffer and - * should be used with caution. - * - * @param in A FILE* that will refer to the process' stdin. - * @param out A FILE* that will refer to the process' stdout. - * @param err A FILE* that will refer to the process' stderr. - * @return A bitwise-or of zero or more of @c pstdin, @c pstdout, @c pstderr. - * @see basic_pstreambuf::fopen() - */ - template - inline std::size_t - pstream_common::fopen(FILE*& fin, FILE*& fout, FILE*& ferr) - { - return buf_.fopen(fin, fout, ferr); + } + if (rpipe(rsrc_out) > -1) { + if ((out = ::fdopen(rpipe(rsrc_out), "r"))) { + open_files |= pstdout; + } + } + if (rpipe(rsrc_err) > -1) { + if ((err = ::fdopen(rpipe(rsrc_err), "r"))) { + open_files |= pstderr; } + } + return open_files; +} -#endif // REDI_EVISCERATE_PSTREAMS +/** + * @warning This function exposes the internals of the stream buffer and + * should be used with caution. + * + * @param in A FILE* that will refer to the process' stdin. + * @param out A FILE* that will refer to the process' stdout. + * @param err A FILE* that will refer to the process' stderr. + * @return A bitwise-or of zero or more of @c pstdin, @c pstdout, @c pstderr. + * @see basic_pstreambuf::fopen() + */ +template +inline std::size_t pstream_common::fopen(FILE*& fin, FILE*& fout, + FILE*& ferr) { + return buf_.fopen(fin, fout, ferr); +} +#endif // REDI_EVISCERATE_PSTREAMS -} // namespace redi +} // namespace redi /** * @mainpage PStreams Reference @@ -2483,4 +2146,3 @@ namespace redi #endif // REDI_PSTREAM_H_SEEN // vim: ts=2 sw=2 expandtab - diff --git a/meson.build b/meson.build index 387199c..15ae0cb 100644 --- a/meson.build +++ b/meson.build @@ -11,7 +11,7 @@ project('efimon', ['c','cpp'], version : '0.2.0', 'c_std=c17', ], license: 'LGPL-2.1-only', - meson_version: '>= 1.0.0' + meson_version: '>= 1.0.0' ) # ----------------------------------------------------------------------------- @@ -100,6 +100,39 @@ else warning('Intel PCM is disabled') endif +# Add disk I/O submodule +enable_disk_io = false +if get_option('enable-disk-io') + disk_io_proj = subproject('disk-io') + disk_io_dep = declare_dependency( + include_directories: disk_io_proj.get_variable('inc'), + dependencies: disk_io_proj.get_variable('libbpf') + ) + project_deps += disk_io_dep + enable_disk_io = true + message('Disk I/O submodule enabled') +else + warning('Disk I/O submodule is disabled') +endif + +# Add Sampling_by_PID submodule +enable_sampling_by_pid = false +if get_option('enable-sampling-by-pid') + sampling_proj = subproject('Sampling_by_PID') + sampling_dep = declare_dependency( + include_directories: sampling_proj.get_variable('projectinc'), + dependencies: [sampling_proj.get_variable('project_deps'), + sampling_proj.get_variable('sampler_dep')] + ) + project_deps += sampling_dep + enable_sampling_by_pid = true + c_args += ['-DENABLE_SAMPLING_BY_PID'] + cpp_args += ['-DENABLE_SAMPLING_BY_PID'] + message('Sampling_by_PID submodule enabled') +else + warning('Sampling_by_PID submodule is disabled') +endif + # Verify if RAPL exists enable_rapl = false if fs.is_dir('/sys/class/powercap/intel-rapl') and get_option('enable-rapl') diff --git a/meson_options.txt b/meson_options.txt index 5e52027..032bc88 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -15,3 +15,5 @@ option('enable-rapl', type: 'boolean', value: true, description: 'Enable the RAP option('enable-perf', type: 'boolean', value: true, description: 'Enable the Linux Perf Tool') option('enable-sql', type: 'boolean', value: true, description: 'Enable the SQL Logger') option('enable-ipmi', type: 'boolean', value: true, description: 'Enable the IPMI Tool') +option('enable-disk-io', type: 'boolean', value: true, description: 'Enable the disk I/O submodule') +option('enable-sampling-by-pid', type: 'boolean', value: true, description: 'Enable the Sampling_by_PID submodule') diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..0586962 --- /dev/null +++ b/src/.gitignore @@ -0,0 +1 @@ +vmlinux.h diff --git a/src/efimon/arg-parser.cpp b/src/efimon/arg-parser.cpp index b96c9e5..5428af7 100644 --- a/src/efimon/arg-parser.cpp +++ b/src/efimon/arg-parser.cpp @@ -6,10 +6,11 @@ * @copyright Copyright (c) 2023. See License for Licensing */ -#include - #include +#include #include +#include +#include namespace efimon { ArgParser::ArgParser(int argc, char **argv) noexcept { diff --git a/src/efimon/asm-classifier.cpp b/src/efimon/asm-classifier.cpp index 4a275e8..e73c671 100644 --- a/src/efimon/asm-classifier.cpp +++ b/src/efimon/asm-classifier.cpp @@ -11,6 +11,7 @@ #include #include #include +#include namespace efimon { diff --git a/src/efimon/asm-classifier/x86-classifier.cpp b/src/efimon/asm-classifier/x86-classifier.cpp index 1faa846..e0096f6 100644 --- a/src/efimon/asm-classifier/x86-classifier.cpp +++ b/src/efimon/asm-classifier/x86-classifier.cpp @@ -14,8 +14,8 @@ namespace efimon { -const std::string x86Classifier::OperandTypes(const std::string &operands) const - noexcept { +const std::string x86Classifier::OperandTypes( + const std::string &operands) const noexcept { std::string res = ""; std::string firstop = ""; std::string secondop = ""; @@ -59,9 +59,8 @@ const std::string x86Classifier::OperandTypes(const std::string &operands) const return res; } -InstructionPair x86Classifier::Classify(const std::string &inst, - const std::string &operands) const - noexcept { +InstructionPair x86Classifier::Classify( + const std::string &inst, const std::string &operands) const noexcept { static const std::string kArithOp[] = {"add", "sub", "div", "mul", "dp", "abs", "sign", "avg", "dec", "inc", "neg"}; diff --git a/src/efimon/ebpf-modules/cpu-assembly-sampler/sampling-by-pid.cpp b/src/efimon/ebpf-modules/cpu-assembly-sampler/sampling-by-pid.cpp new file mode 100644 index 0000000..0c9b993 --- /dev/null +++ b/src/efimon/ebpf-modules/cpu-assembly-sampler/sampling-by-pid.cpp @@ -0,0 +1,525 @@ +/** + * @file sampling-by-pid.cpp + * @author Diego Avila (diego.avila@uned.cr) + * @brief Sampling-by-PID eBPF-based CPU cycle sampler observer + * + * @copyright Copyright (c) 2026. See License for Licensing + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "prog.skel.h" // NOLINT + +namespace efimon { + +/** + * @brief eBPF sample structure matching the kernel-side sample_t + */ +struct BPFSample { + uint32_t pid; + uint32_t tid; + uint64_t ip; + uint64_t ts; +}; + +/** + * @brief Callback context for the ring buffer + */ +struct RBContext { + SamplingByPIDObserver *observer; +}; + +static int perf_event_open(struct perf_event_attr *attr, pid_t pid, int cpu, + int group_fd, uint64_t flags) { + return static_cast( + syscall(__NR_perf_event_open, attr, pid, cpu, group_fd, flags)); +} + +/** + * @brief Ring buffer callback: just collect the raw sample into a vector + */ +int handle_rb_event(void *ctx, void *data, size_t size) { + if (size != sizeof(BPFSample) || !ctx || !data) return 0; + auto *rb_ctx = static_cast(ctx); + auto *s = static_cast(data); + rb_ctx->observer->collected_samples_.push_back( + {s->pid, s->tid, s->ip, s->ts}); + return 0; +} + +SamplingByPIDObserver::SamplingByPIDObserver(const uint pid, + const ObserverScope scope, + const uint64_t interval, + const uint64_t frequency) + : Observer{}, + readings_{}, + frequency_{frequency}, + valid_{false}, + ring_buffer_fd_{-1}, + bpf_skeleton_{nullptr}, + current_ip_{0}, + current_sample_tid_{0}, + current_sample_ts_{0}, + inst_mem_{}, + samples_collected_{0} { + this->pid_ = pid; + this->interval_ = interval; + this->worker_running_.store(false); + this->worker_thread_ = nullptr; + + uint64_t type = static_cast(ObserverType::CPU) | + static_cast(ObserverType::INTERVAL) | + static_cast(ObserverType::CPU_INSTRUCTIONS); + + if (ObserverScope::PROCESS != scope) { + throw Status{Status::INVALID_PARAMETER, "System-scope is not supported"}; + } + + this->caps_.emplace_back(); + this->caps_[0].type = type; + +#if defined(__x86_64__) || defined(_M_X64) || defined(i386) || \ + defined(__i386__) || defined(__i386) || defined(_M_IX86) + this->classifier_ = AsmClassifier::Build(assembly::Architecture::X86); +#else + this->classifier_ = nullptr; +#endif + + this->Reset(); +} + +Status SamplingByPIDObserver::InitializeBPF() { + /* Open and load the eBPF skeleton */ + auto *skel = prog_bpf__open_and_load(); + if (!skel) { + return Status{Status::CONFIGURATION_ERROR, + "Failed to open/load BPF skeleton"}; + } + this->bpf_skeleton_ = static_cast(skel); + + int ncpus = sysconf(_SC_NPROCESSORS_ONLN); + if (ncpus <= 0) ncpus = 1; + + int prog_fd = bpf_program__fd(skel->progs.on_sample); + bool attached = false; + int last_errno = 0; + + this->perf_fds_.resize(ncpus, -1); + + /* Cap frequency to the kernel max to avoid EINVAL */ + uint64_t freq = this->frequency_; + std::ifstream max_rate_file("/proc/sys/kernel/perf_event_max_sample_rate"); + if (max_rate_file.is_open()) { + uint64_t max_rate = 0; + max_rate_file >> max_rate; + if (max_rate > 0 && freq > max_rate) { + freq = max_rate; + } + } + + /* + * Try multiple perf event configurations with decreasing precision. + * precise_ip=2 requires PEBS which may not be available on all hardware + * or in virtual machines. Fall back to software CPU clock if hardware + * counters are unavailable. + */ + struct { + uint32_t type; + uint64_t config; + uint32_t precise_ip; + } attempts[] = { + {PERF_TYPE_HARDWARE, PERF_COUNT_HW_CPU_CYCLES, 2}, + {PERF_TYPE_HARDWARE, PERF_COUNT_HW_CPU_CYCLES, 0}, + {PERF_TYPE_SOFTWARE, PERF_COUNT_SW_CPU_CLOCK, 0}, + }; + + for (const auto &attempt : attempts) { + /* Reset state from previous attempt */ + for (int cpu = 0; cpu < ncpus; cpu++) { + if (this->perf_fds_[cpu] >= 0) { + close(this->perf_fds_[cpu]); + this->perf_fds_[cpu] = -1; + } + } + attached = false; + + struct perf_event_attr attr {}; + memset(&attr, 0, sizeof(attr)); + attr.type = attempt.type; + attr.config = attempt.config; + attr.size = sizeof(attr); + attr.sample_type = PERF_SAMPLE_IP | PERF_SAMPLE_TID | PERF_SAMPLE_TIME; + attr.freq = 1; + attr.sample_freq = freq; + attr.precise_ip = attempt.precise_ip; + attr.disabled = 0; + attr.exclude_kernel = 1; + attr.exclude_hv = 1; + + for (int cpu = 0; cpu < ncpus; cpu++) { + this->perf_fds_[cpu] = + perf_event_open(&attr, static_cast(this->pid_), cpu, -1, 0); + if (this->perf_fds_[cpu] < 0) { + last_errno = errno; + continue; + } + + if (ioctl(this->perf_fds_[cpu], PERF_EVENT_IOC_SET_BPF, prog_fd) < 0) { + last_errno = errno; + close(this->perf_fds_[cpu]); + this->perf_fds_[cpu] = -1; + continue; + } + + if (ioctl(this->perf_fds_[cpu], PERF_EVENT_IOC_ENABLE, 0) < 0) { + last_errno = errno; + close(this->perf_fds_[cpu]); + this->perf_fds_[cpu] = -1; + continue; + } + + attached = true; + } + + if (attached) break; + } + + if (!attached) { + std::string errmsg = "Failed to attach perf events for target PID: "; + errmsg += strerror(last_errno); + errmsg += " (errno=" + std::to_string(last_errno) + ")"; + this->CleanupBPF(); + return Status{Status::ACCESS_DENIED, errmsg}; + } + + /* Create the ring buffer to receive samples from the eBPF program */ + this->ring_buffer_fd_ = bpf_map__fd(skel->maps.rb); + + return Status{}; +} + +Status SamplingByPIDObserver::CleanupBPF() { + /* Close perf event file descriptors */ + for (int fd : this->perf_fds_) { + if (fd >= 0) close(fd); + } + this->perf_fds_.clear(); + + /* Destroy the BPF skeleton */ + if (this->bpf_skeleton_) { + prog_bpf__destroy(static_cast(this->bpf_skeleton_)); + this->bpf_skeleton_ = nullptr; + } + + this->ring_buffer_fd_ = -1; + return Status{}; +} + +Status SamplingByPIDObserver::PollRingBuffer() { + if (this->ring_buffer_fd_ < 0) { + return Status{Status::NOT_READY, "Ring buffer not initialized"}; + } + + auto *skel = static_cast(this->bpf_skeleton_); + RBContext rb_ctx{this}; + struct ring_buffer *rb = + ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_rb_event, + static_cast(&rb_ctx), nullptr); + if (!rb) { + return Status{Status::CONFIGURATION_ERROR, + "Failed to create ring buffer consumer"}; + } + + /* Poll until the worker is stopped */ + while (this->worker_running_.load()) { + ring_buffer__poll(rb, 100); + } + + /* Drain remaining events */ + ring_buffer__poll(rb, 0); + + ring_buffer__free(rb); + return Status{}; +} + +Status SamplingByPIDObserver::DecodeInstruction(const uint64_t ip) { + /* Read the memory from the process at the given IP */ + char filename[64]; + snprintf(filename, sizeof(filename), "/proc/%d/mem", this->pid_); + int fd = open(filename, O_RDONLY); + if (fd == -1) { + return Status{Status::CANNOT_OPEN, + "The memory from the target process cannot be opened"}; + } + + ssize_t size = sizeof(this->inst_mem_); + if (pread(fd, this->inst_mem_, size, static_cast(ip)) != size) { + close(fd); + return Status{Status::CANNOT_OPEN, + "The memory from the target process cannot be read"}; + } + close(fd); + + /* Decode using Capstone */ + csh handle = 0; + cs_insn *insn = nullptr; + size_t count = 0; + + if (cs_open(CS_ARCH_X86, CS_MODE_64, &handle) != CS_ERR_OK) { + return Status{Status::CONFIGURATION_ERROR, "Cannot initialise Capstone"}; + } + + cs_option(handle, CS_OPT_SYNTAX, CS_OPT_SYNTAX_ATT); + + count = + cs_disasm(handle, this->inst_mem_, sizeof(this->inst_mem_), ip, 1, &insn); + + if (count <= 0) { + cs_close(&handle); + return Status{Status::CANNOT_OPEN, "Cannot decode the instruction"}; + } + + this->inst_ = + std::string(insn[0].mnemonic) + " " + std::string(insn[0].op_str); + cs_free(insn, count); + cs_close(&handle); + + return Status{}; +} + +Status SamplingByPIDObserver::ProcessSample(const uint32_t /* pid */, + const uint32_t /* tid */, + const uint64_t ip, + const uint64_t /* ts */) { + this->current_ip_ = ip; + + /* Skip kernel addresses (x86_64 kernel space starts at 0xffff800000000000) */ + if (ip >= 0xffff800000000000ULL || ip == 0) { + return Status{}; + } + + auto decode_ret = this->DecodeInstruction(ip); + if (decode_ret.code != Status::OK) { + /* Skip samples that cannot be decoded */ + return Status{}; + } + + this->ParseResults(); + this->samples_collected_++; + return Status{}; +} + +Status SamplingByPIDObserver::ParseResults() { + std::stringstream sloc; + std::string assembly; + std::string operands; + + sloc << this->inst_; + sloc >> assembly; + operands = sloc.str(); + + if (!this->classifier_) + return Status{Status::MEMBER_ABSENT, "Cannot get classifier"}; + + std::string optypes = this->classifier_->OperandTypes(operands); + InstructionPair classification = + this->classifier_->Classify(assembly, optypes); + assembly += std::string("_") + optypes; + + /* Add to the histogram */ + if (this->readings_.histogram.find(assembly) == + this->readings_.histogram.end()) { + this->readings_.histogram[assembly] = 0; + } + + /* Handle the creation of the classification maps */ + bool family_found = + this->readings_.classification[std::get<0>(classification)].find( + std::get<1>(classification)) != + this->readings_.classification[std::get<0>(classification)].end(); + if (!family_found) { + this->readings_.classification[std::get<0>(classification)] + [std::get<1>(classification)] = {}; + } + bool origin_found = this->readings_ + .classification[std::get<0>(classification)] + [std::get<1>(classification)] + .find(std::get<2>(classification)) != + this->readings_ + .classification[std::get<0>(classification)] + [std::get<1>(classification)] + .end(); + if (!origin_found) { + this->readings_.classification[std::get<0>(classification)][std::get<1>( + classification)][std::get<2>(classification)] = 0.f; + } + + this->readings_.classification[std::get<0>(classification)][std::get<1>( + classification)][std::get<2>(classification)] += 100.f; + this->readings_.histogram[assembly] += 100.f; + + this->valid_ = true; + return Status{}; +} + +Status SamplingByPIDObserver::NormalizeResults() { + if (this->samples_collected_ == 0) return Status{}; + + for (auto &pair : this->readings_.histogram) { + pair.second /= this->samples_collected_; + } + + for (auto &type : this->readings_.classification) { + for (auto &family : type.second) { + for (auto &origin : family.second) { + origin.second /= this->samples_collected_; + } + } + } + + return Status{}; +} + +Status SamplingByPIDObserver::Trigger() { + Status ret{}; + + /* Clear the histogram */ + this->readings_.histogram.clear(); + this->readings_.classification.clear(); + this->samples_collected_ = 0; + this->collected_samples_.clear(); + + /* Initialize eBPF program and perf events */ + ret = this->InitializeBPF(); + if (ret.code != Status::OK) return ret; + + /* Launch the worker to poll the ring buffer and collect raw samples */ + this->worker_running_.store(true); + this->worker_thread_ = + std::make_unique(&SamplingByPIDObserver::Worker, this); + + /* Wait for the specified interval */ + std::this_thread::sleep_for(std::chrono::milliseconds(this->interval_)); + + /* Signal the worker to stop polling */ + this->worker_running_.store(false); + + { + std::unique_lock lk(this->worker_mutex_); + this->worker_cv_.wait_for(lk, std::chrono::seconds(5)); + } + + this->worker_thread_->join(); + this->worker_thread_.reset(nullptr); + + /* Clean up eBPF resources before processing (perf events no longer needed) */ + this->CleanupBPF(); + + /* Now process the collected samples (decode + classify) */ + for (const auto &sample : this->collected_samples_) { + this->ProcessSample(sample.pid, sample.tid, sample.ip, sample.ts); + } + + this->NormalizeResults(); + return Status{}; +} + +void SamplingByPIDObserver::Worker() { + /* Poll the eBPF ring buffer, collecting raw samples into collected_samples_ + */ + this->PollRingBuffer(); + this->worker_cv_.notify_one(); +} + +std::vector SamplingByPIDObserver::GetReadings() { + std::scoped_lock lock(this->worker_mutex_); + return std::vector{static_cast(&(this->readings_))}; +} + +Status SamplingByPIDObserver::SelectDevice(const uint /* device */) { + return Status{ + Status::NOT_IMPLEMENTED, + "It is not possible to select a device since this is a wrapper class"}; +} + +Status SamplingByPIDObserver::SetScope(const ObserverScope /* scope */) { + return Status{ + Status::NOT_IMPLEMENTED, + "It is not possible change the scope since this is a wrapper class"}; +} + +Status SamplingByPIDObserver::SetPID(const uint pid) { + this->pid_ = pid; + return Status{}; +} + +ObserverScope SamplingByPIDObserver::GetScope() const noexcept { + return ObserverScope::PROCESS; +} + +uint SamplingByPIDObserver::GetPID() const noexcept { return this->pid_; } + +const std::vector + &SamplingByPIDObserver::GetCapabilities() const noexcept { + return this->caps_; +} + +Status SamplingByPIDObserver::GetStatus() { return Status{}; } + +Status SamplingByPIDObserver::SetInterval(const uint64_t interval) { + this->interval_ = interval; + return Status{}; +} + +Status SamplingByPIDObserver::ClearInterval() { + this->interval_ = 0; + return Status{Status::OK, "The clear interval has been cleared"}; +} + +Status SamplingByPIDObserver::Reset() { + this->readings_.timestamp = 0; + this->readings_.difference = 0; + this->valid_ = false; + this->readings_.type = static_cast(ObserverType::CPU); + + /* Clear the histogram */ + this->readings_.histogram.clear(); + this->readings_.classification.clear(); + this->samples_collected_ = 0; + return Status{}; +} + +SamplingByPIDObserver::~SamplingByPIDObserver() { + if (this->worker_running_.load()) { + this->worker_running_.store(false); + if (this->worker_thread_ && this->worker_thread_->joinable()) { + this->worker_thread_->join(); + } + } + this->CleanupBPF(); +} + +} /* namespace efimon */ diff --git a/src/efimon/logger/csv.cpp b/src/efimon/logger/csv.cpp index 1f2c4a0..da4a43f 100644 --- a/src/efimon/logger/csv.cpp +++ b/src/efimon/logger/csv.cpp @@ -13,6 +13,7 @@ #include #include #include +#include namespace efimon { diff --git a/src/efimon/logger/sqlite.cpp b/src/efimon/logger/sqlite.cpp index 2b55538..77478bd 100644 --- a/src/efimon/logger/sqlite.cpp +++ b/src/efimon/logger/sqlite.cpp @@ -13,6 +13,7 @@ #include #include #include +#include namespace efimon { static std::unordered_map kSqlMapping = { diff --git a/src/efimon/meson.build b/src/efimon/meson.build index f8e6407..fa80abb 100644 --- a/src/efimon/meson.build +++ b/src/efimon/meson.build @@ -63,3 +63,9 @@ if enable_ipmi and enable_ipmi_sensors files('power/ipmi.cpp'), ] endif + +if enable_sampling_by_pid + lib_efimon_sources += [ + files('ebpf-modules/cpu-assembly-sampler/sampling-by-pid.cpp'), + ] +endif diff --git a/src/efimon/perf/annotate.cpp b/src/efimon/perf/annotate.cpp index bf185df..ec0b7c2 100644 --- a/src/efimon/perf/annotate.cpp +++ b/src/efimon/perf/annotate.cpp @@ -17,9 +17,8 @@ #include #include #include -#include - #include +#include #ifndef PERF_ANNOTATE_THRES #define PERF_ANNOTATE_THRES 0.01 @@ -191,8 +190,9 @@ uint PerfAnnotateObserver::GetPID() const noexcept { return this->record_.GetPID(); } -const std::vector& PerfAnnotateObserver::GetCapabilities() - const noexcept { +const std::vector& +PerfAnnotateObserver::GetCapabilities() // NOLINT + const noexcept { // NOLINT return this->caps_; } diff --git a/src/efimon/perf/record.cpp b/src/efimon/perf/record.cpp index 45d3552..e5763c9 100644 --- a/src/efimon/perf/record.cpp +++ b/src/efimon/perf/record.cpp @@ -8,12 +8,14 @@ */ #include +#include #include -#include -#include // NOLINT +#include // NOLINT(build/c++17) +#include // NOLINT #include #include #include +#include static std::mutex singleton_mutex_; static std::vector active_pids_; diff --git a/src/efimon/power/intel.cpp b/src/efimon/power/intel.cpp index 0136839..c103c21 100644 --- a/src/efimon/power/intel.cpp +++ b/src/efimon/power/intel.cpp @@ -15,6 +15,7 @@ #include // NOLINT #include #include +#include #include namespace efimon { diff --git a/src/efimon/power/rapl.cpp b/src/efimon/power/rapl.cpp index 7d756fa..533a7c7 100644 --- a/src/efimon/power/rapl.cpp +++ b/src/efimon/power/rapl.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include namespace efimon { diff --git a/src/efimon/proc/cpuinfo.cpp b/src/efimon/proc/cpuinfo.cpp index a82b35c..d31fe06 100644 --- a/src/efimon/proc/cpuinfo.cpp +++ b/src/efimon/proc/cpuinfo.cpp @@ -13,17 +13,19 @@ #include #include #include +#include namespace efimon { +// NOLINT static constexpr char kCpuInfoFile[] = "/proc/cpuinfo"; static std::mutex m_single_cpuinfo; CPUInfo::CPUInfo() : num_logical_cores_{0}, - num_physical_cores_{0}, - num_sockets_{0}, - topology_{} { + num_physical_cores_{0}, // NOLINT + num_sockets_{0}, // NOLINT + topology_{} { // NOLINT /* Parses the map and constructs it */ ParseMap(); diff --git a/src/efimon/proc/list.cpp b/src/efimon/proc/list.cpp index adf6a41..17e8b22 100644 --- a/src/efimon/proc/list.cpp +++ b/src/efimon/proc/list.cpp @@ -6,14 +6,15 @@ * @copyright Copyright (c) 2024. See License for Licensing */ -#include -#include +#include +#include #include #include - -#include -#include +#include +#include +#include +#include namespace efimon { diff --git a/src/efimon/proc/meminfo.cpp b/src/efimon/proc/meminfo.cpp index e911dac..bb78456 100644 --- a/src/efimon/proc/meminfo.cpp +++ b/src/efimon/proc/meminfo.cpp @@ -8,6 +8,7 @@ #include +#include #include #include #include diff --git a/src/efimon/proc/net.cpp b/src/efimon/proc/net.cpp index 44f54fa..be2ee37 100644 --- a/src/efimon/proc/net.cpp +++ b/src/efimon/proc/net.cpp @@ -8,6 +8,7 @@ #include +#include #include #include #include diff --git a/src/efimon/proc/process-tree.cpp b/src/efimon/proc/process-tree.cpp index 88d9823..f18123f 100644 --- a/src/efimon/proc/process-tree.cpp +++ b/src/efimon/proc/process-tree.cpp @@ -7,11 +7,12 @@ */ #include -#include +#include // NOLINT(build/c++17) #include #include #include #include +#include namespace efimon { diff --git a/src/efimon/proc/stat.cpp b/src/efimon/proc/stat.cpp index e316800..a875430 100644 --- a/src/efimon/proc/stat.cpp +++ b/src/efimon/proc/stat.cpp @@ -18,6 +18,7 @@ #include // NOLINT #include #include +#include #include extern std::mutex m_single_uptime; diff --git a/src/efimon/process-manager.cpp b/src/efimon/process-manager.cpp index ef07be5..10a5065 100644 --- a/src/efimon/process-manager.cpp +++ b/src/efimon/process-manager.cpp @@ -7,10 +7,10 @@ */ #include - -#include - #include +#include +#include +#include namespace efimon { diff --git a/src/tools/efimon-meter.cpp b/src/tools/efimon-meter.cpp index bb2c6da..4b5bcd5 100644 --- a/src/tools/efimon-meter.cpp +++ b/src/tools/efimon-meter.cpp @@ -8,8 +8,6 @@ #include -#include - #include #include #include @@ -17,6 +15,7 @@ #include #include #include +#include int main(int argc, char **argv) { auto cli = efimon::ArgParser{argc, argv}; diff --git a/src/tools/efimon-power-analyser.cpp b/src/tools/efimon-power-analyser.cpp index cdb5b5e..8ef593f 100644 --- a/src/tools/efimon-power-analyser.cpp +++ b/src/tools/efimon-power-analyser.cpp @@ -23,6 +23,7 @@ #include +#include #include #include // NOLINT #include @@ -37,10 +38,13 @@ #include #include #include +#include #include // NOLINT #include #include #include // NOLINT +#include +#include using namespace efimon; // NOLINT diff --git a/subprojects/.wraplock b/subprojects/.wraplock new file mode 100644 index 0000000..e69de29 diff --git a/subprojects/Sampling_by_PID/.gitignore b/subprojects/Sampling_by_PID/.gitignore new file mode 100644 index 0000000..ccf044a --- /dev/null +++ b/subprojects/Sampling_by_PID/.gitignore @@ -0,0 +1,30 @@ +# Build directory (generated by meson) +builddir/ +build/ + +# Auto-generated eBPF files +src/vmlinux.h +src/prog.bpf.o +src/prog.skel.h +include/prog.skel.h + +# Compiled binaries +src/sampler +src/benchmark + +# CMake and other build artifacts +CMakeFiles/ +cmake_install.cmake +CMakeCache.txt + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +.AppleDouble +.LSOverride diff --git a/subprojects/Sampling_by_PID/README.md b/subprojects/Sampling_by_PID/README.md new file mode 100644 index 0000000..6cbc671 --- /dev/null +++ b/subprojects/Sampling_by_PID/README.md @@ -0,0 +1,112 @@ +# eBPF Sampler & Benchmark Project +Este proyecto implementa un sistema de muestreo de rendimiento basado en **eBPF**. +Permite monitorear procesos en ejecución, recolectar métricas y evaluar el impacto (slowdown) sobre el rendimiento mediante un **benchmark** y visualización de resultados. + +--- + +## Estructura del proyecto +EfiMon.CPUConsumptionModule/ +│ +├── src/ +│ ├── main.cpp # Programa principal en espacio de usuario +│ ├── prog.bpf.c # Programa eBPF (kernel space) +│ └── benchmark.c # Código C para generar carga (multiplicación de matrices) +│ +├── include/ +│ ├── vmlinux.h # Definición de estructuras del kernel (generado) +│ └── prog.skel.h # Skeleton autogenerado por bpftool +│ +├── build/ +│ ├── prog.bpf.o # Objeto compilado del eBPF +│ ├── main # Binario compilado del programa en user-space +│ └── benchmark # Binario compilado del benchmark +│ +├── data/ +│ ├── results/ +│ │ ├── bench_no_sampler.out # Archivos de salida de las pruebas +│ │ ├── bench_with_sampler.out +│ │ └── results.csv # Resultados crudos del benchmark +│ └── plots/ +│ └── slowdown_vs_freq.png # Imagen del plot con los resultados de las pruebas +│ +├── scripts/ +│ └── run_tests.sh # Script para ejecutar el benchmark +│ └── analyze_results.py # Script para generar gráficas con matplotlib +│ +├── Makefile # Archivo CMake para compilación y ejecución +└── README.md # Instrucciones generales del proyecto + +--- + +## Dependencias +Asegúrate de tener instaladas las siguientes herramientas antes de compilar: + +### Paquetes del sistema +```bash +sudo apt update +sudo apt install clang llvm libbpf-dev libelf-dev zlib1g-dev gcc g++ bpftool python3-matplotlib python3-pandas libopenblas-dev +``` + +### Requisitos adicionales +- Kernel con soporte eBPF (5.8+ recomendado) +- Permisos para ejecutar bpftool y cargar programas eBPF (usualmente requiere sudo) + +## Compilación +Para construir todos los componentes (eBPF, sampler, benchmark y skeleton): +```bash +make all +``` + +Esto generará: +- include/vmlinux.h — definición del kernel +- build/prog.bpf.o — bytecode eBPF +- include/prog.skel.h — skeleton generado automáticamente +- build/sampler — programa en espacio de usuario +- build/benchmark — programa de benchmark + +## Ejecución +1. Ejecutar el sampler sobre un proceso +Para monitorear un proceso específico (por su PID): +```bash +sudo make run PID= F= D= +``` + +2. Ejecutarlo de forma manual +```bash +./build/sampler +``` + +## Ejecución de pruebas +El proyecto incluye un script que ejecuta un benchmark: +```bash +make test +``` + +Este guarda los resultados en: data/results.csv + +**NOTA: Para ver y obtener mejores resultados (principalmente en el plot), se recomienda ejecutar las pruebas varias veces con distintas frecuencias de muestreo. Puedes modificar el valor de FREQ editando el archivo: scripts/run_tests.sh** + +## Visualización de resultados +Para generar los gráficos: +```bash +make plot +``` + +El script scripts/analyze_results.py lee el archivo CSV con los resultados y produce una gráfica slowdown vs frequency en: +```bash +data/plots/ +``` + +## Limpieza +Para eliminar todos los archivos generados (builds, skeletons, etc.): +```bash +make clean +``` + +## Tecnologías utilizadas +- eBPF / libbpf: recolección eficiente de métricas desde el kernel +- C / C++: componentes en espacio de usuario y benchmark +- bpftool: generación automática de headers (vmlinux.h, skeleton) +- Python (matplotlib, pandas): visualización de resultados +- OpenBLAS: operaciones de cómputo intensivo en el benchmark + diff --git a/subprojects/Sampling_by_PID/include/meson.build b/subprojects/Sampling_by_PID/include/meson.build new file mode 100644 index 0000000..e41bd23 --- /dev/null +++ b/subprojects/Sampling_by_PID/include/meson.build @@ -0,0 +1 @@ +# vmlinux.h is generated in main meson.build diff --git a/subprojects/Sampling_by_PID/include/sampler_lib.hpp b/subprojects/Sampling_by_PID/include/sampler_lib.hpp new file mode 100644 index 0000000..8f21d49 --- /dev/null +++ b/subprojects/Sampling_by_PID/include/sampler_lib.hpp @@ -0,0 +1,46 @@ +/** + * @file sampler_lib.hpp + * @author Diego Avila + * Anthony Montero + * @brief CPU sampling library interface using eBPF + * + * @copyright Copyright (c) 2026. See License for Licensing + */ + +#pragma once + +#include +#include +#include + +namespace cpu_sampler { + +struct Sample { + uint32_t pid; + uint32_t tid; + uint64_t ip; + uint64_t ts; +}; + +struct Config { + int target_pid; + uint64_t frequency_hz; + int duration_seconds; +}; + +// Request a graceful stop for a currently running sampling session. +void request_stop(); + +/** + * @brief Runs eBPF perf-event sampling and appends collected samples to + * v_out_samples. + * + * @param st_config Configuration for the sampling session + * @param v_out_samples Vector to store collected samples + * @param str_error_message String to store error messages in case of failure + * @return true on success, false on failure + */ +bool run_sampling(const Config& st_config, std::vector& v_out_samples, + std::string& str_error_message); + +} // namespace cpu_sampler diff --git a/subprojects/Sampling_by_PID/meson.build b/subprojects/Sampling_by_PID/meson.build new file mode 100644 index 0000000..61becb4 --- /dev/null +++ b/subprojects/Sampling_by_PID/meson.build @@ -0,0 +1,19 @@ +project('CPUConsumptionModule', version: '0.3.1', + default_options: [ + 'cpp_std=c++17', + 'c_std=c17' + ]) +add_languages('c', 'cpp') + +clang = find_program('clang') +bpftool = find_program('bpftool') +libbpf_dep = dependency('libbpf', required: true) + + +projectinc = [include_directories('.', 'include')] + +project_deps = [libbpf_dep] + + + +subdir('src') diff --git a/subprojects/Sampling_by_PID/src/.gitignore b/subprojects/Sampling_by_PID/src/.gitignore new file mode 100644 index 0000000..621b161 --- /dev/null +++ b/subprojects/Sampling_by_PID/src/.gitignore @@ -0,0 +1,8 @@ +# Auto-generated eBPF files (generated by meson during build) +vmlinux.h +prog.bpf.o +prog.skel.h + +# Compiled binaries +sampler +benchmark diff --git a/subprojects/Sampling_by_PID/src/benchmark.c b/subprojects/Sampling_by_PID/src/benchmark.c new file mode 100644 index 0000000..83ea256 --- /dev/null +++ b/subprojects/Sampling_by_PID/src/benchmark.c @@ -0,0 +1,79 @@ +/** + * @file benchmark.c + * @author Diego Avila + * Anthony Montero + * @brief Benchmark program for CPU consumption testing + * + * @copyright Copyright (c) 2026. See License for Licensing + */ + +#define _POSIX_C_SOURCE 200809L + +#include +#include +#include +#include + +static double now_sec(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return ts.tv_sec + ts.tv_nsec * 1e-9; +} + +int main(int argc, char **argv) { + if (argc < 4) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + int N = atoi(argv[1]); // matrix size: N x N + int reps = atoi(argv[2]); // number of repetitions to average + int warmup = atoi(argv[3]); // warmup runs before measured reps + + // allocate matrices (row-major for cblas, but using column-major calls + // consistent with BLAS) + size_t elems = (size_t)N * (size_t)N; + double *A = aligned_alloc(64, elems * sizeof(double)); + double *B = aligned_alloc(64, elems * sizeof(double)); + double *C = aligned_alloc(64, elems * sizeof(double)); + if (!A || !B || !C) { + fprintf(stderr, "Allocation failed\n"); + return 1; + } + + // init with random values (deterministic seed) + srand(42); + for (size_t i = 0; i < elems; ++i) { + A[i] = ((double)rand() / RAND_MAX); + B[i] = ((double)rand() / RAND_MAX); + C[i] = 0.0; + } + + // Warmup runs (not measured) + for (int w = 0; w < warmup; ++w) { + cblas_dgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans, N, N, N, 1.0, A, N, + B, N, 0.0, C, N); + } + + // Measured reps + double t0 = now_sec(); + for (int r = 0; r < reps; ++r) { + cblas_dgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans, N, N, N, 1.0, A, N, + B, N, 0.0, C, N); + } + double t1 = now_sec(); + + double elapsed = (t1 - t0); // average time per multiplication (seconds) + + // FLOPs for one matrix multiply (approx): 2 * N^3 + double flops = 2.0 * (double)N * (double)N * (double)N; + double gflops = (flops / elapsed) / 1e9; + + // Print a succinct output + printf("N=%d reps=%d warmup=%d time_s=%.9f gflops=%.6f\n", N, reps, warmup, + elapsed, gflops); + + free(A); + free(B); + free(C); + return 0; +} diff --git a/subprojects/Sampling_by_PID/src/meson.build b/subprojects/Sampling_by_PID/src/meson.build new file mode 100644 index 0000000..6ec9e3a --- /dev/null +++ b/subprojects/Sampling_by_PID/src/meson.build @@ -0,0 +1,68 @@ +bpf_o = 'prog.bpf.o' +bpf_skel_h = 'prog.skel.h' + +# Generate vmlinux.h first +vmlinux = custom_target('vmlinux', + command: [ + 'sh', '-c', + 'bpftool btf dump file /sys/kernel/btf/vmlinux format c > @OUTPUT@' + ], + output: 'vmlinux.h', +) + + +# Compile eBPF program +bpf_prog = custom_target('bpf_prog', + input: 'prog.bpf.c', + output: bpf_o, + command: [ + clang, + '-target', 'bpf', + '-D__TARGET_ARCH_x86', + '-O2', '-g', + '-I', meson.current_source_dir(), + '-I', meson.current_build_dir(), + '-c', '@INPUT@', + '-o', '@OUTPUT@'], + depends: vmlinux +) + +# Generate skeleton from eBPF object +skeleton = custom_target('bpf_skeleton', + command: ['bpftool', 'gen', 'skeleton', '@INPUT@'], + input: bpf_prog, + output: bpf_skel_h, + capture: true, + build_by_default: true +) + +# Create a dependency on the skeleton generation +skeleton_dep = declare_dependency( + include_directories: include_directories('.'), + sources: skeleton +) + +# Reusable sampler library (replace main-based executable) +sampler_lib = static_library('cpu_sampler', + 'sampler_lib.cpp', + include_directories: [projectinc, include_directories('.' / '..' / 'include')], + dependencies: [project_deps, skeleton_dep], + cpp_args: ['-O0', '-g', '-Wall'], + install: true +) + +sampler_dep = declare_dependency( + link_with: sampler_lib, + include_directories: [projectinc, include_directories('.' / '..' / 'include')], + dependencies: [project_deps, skeleton_dep] +) + +install_headers('../include/sampler_lib.hpp', subdir: 'cpuconsumptionmodule') + +# Benchmark executable +benchmark_exe = executable('bench', + 'benchmark.c', + c_args: ['-O0', '-I/usr/include/openblas'], + link_args: ['-lopenblas', '-lm'], + install: true +) diff --git a/subprojects/Sampling_by_PID/src/prog.bpf.c b/subprojects/Sampling_by_PID/src/prog.bpf.c new file mode 100644 index 0000000..9e7d7c9 --- /dev/null +++ b/subprojects/Sampling_by_PID/src/prog.bpf.c @@ -0,0 +1,50 @@ +/** + * @file prog.bpf.c + * @author Diego Avila + * Anthony Montero + * @brief eBPF program for CPU cycle sampling via perf events + * + * @copyright Copyright (c) 2026. See License for Licensing + */ + +// prog.bpf.c + +// clang-format off +#include "vmlinux.h" // NOLINT +#include +#include +#include +// clang-format on + +struct sample_t { + u32 pid; + u32 tid; + u64 ip; + u64 ts; +}; + +struct { + __uint(type, BPF_MAP_TYPE_RINGBUF); + __uint(max_entries, 1 << 24); // 16MB +} rb SEC(".maps"); + +SEC("perf_event") +int on_sample(struct bpf_perf_event_data *ctx) { + struct sample_t *s; + u64 pid_tgid = bpf_get_current_pid_tgid(); + + s = bpf_ringbuf_reserve(&rb, sizeof(*s), 0); + if (!s) return 0; + + s->pid = pid_tgid >> 32; + s->tid = (u32)pid_tgid; + s->ts = bpf_ktime_get_ns(); + + // Capture the Program Counter (IP) + s->ip = PT_REGS_IP(&ctx->regs); + + bpf_ringbuf_submit(s, 0); + return 0; +} + +char LICENSE[] SEC("license") = "GPL"; diff --git a/subprojects/Sampling_by_PID/src/sampler_lib.cpp b/subprojects/Sampling_by_PID/src/sampler_lib.cpp new file mode 100644 index 0000000..38cae66 --- /dev/null +++ b/subprojects/Sampling_by_PID/src/sampler_lib.cpp @@ -0,0 +1,171 @@ +/** + * @file sampler_lib.cpp + * @author Diego Avila + * Anthony Montero + * @brief CPU sampling library implementation using eBPF + * + * @copyright Copyright (c) 2026. See License for Licensing + */ + +#include "sampler_lib.hpp" // NOLINT + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "../include/prog.skel.h" + +namespace cpu_sampler { + +static volatile sig_atomic_t g_i_stop = 0; + +static void sig_handler(int) { g_i_stop = 1; } + +void request_stop() { g_i_stop = 1; } + +static int perf_event_open(struct perf_event_attr* attr, pid_t pid, int cpu, + int group_fd, uint64_t flags) { + return syscall(__NR_perf_event_open, attr, pid, cpu, group_fd, flags); +} + +struct callback_ctx_t { + std::vector* p_out; +}; + +static int handle_event(void* ctx, void* data, size_t size) { + if (size != sizeof(Sample) || !ctx || !data) { + return 0; + } + + auto* p_cb = static_cast(ctx); + p_cb->p_out->push_back(*static_cast(data)); + return 0; +} + +bool run_sampling(const Config& st_config, std::vector& v_out_samples, + std::string& str_error_message) { + str_error_message.clear(); + + if (st_config.target_pid <= 0) { + str_error_message = "invalid target_pid"; + return false; + } + if (st_config.frequency_hz == 0) { + str_error_message = "frequency_hz must be > 0"; + return false; + } + if (st_config.duration_seconds <= 0) { + str_error_message = "duration_seconds must be > 0"; + return false; + } + + g_i_stop = 0; + signal(SIGINT, sig_handler); + signal(SIGTERM, sig_handler); + + prog_bpf* p_skel = prog_bpf__open_and_load(); + if (!p_skel) { + str_error_message = "failed to open/load BPF skeleton"; + return false; + } + + struct perf_event_attr st_attr {}; + memset(&st_attr, 0, sizeof(st_attr)); + st_attr.type = PERF_TYPE_HARDWARE; + st_attr.config = PERF_COUNT_HW_CPU_CYCLES; + st_attr.size = sizeof(st_attr); + st_attr.sample_type = PERF_SAMPLE_IP | PERF_SAMPLE_TID | PERF_SAMPLE_TIME; + st_attr.freq = 1; + st_attr.sample_freq = st_config.frequency_hz; + st_attr.precise_ip = 2; + st_attr.disabled = 0; + + int i_cpu_count = sysconf(_SC_NPROCESSORS_ONLN); + if (i_cpu_count <= 0) { + i_cpu_count = 1; + } + + std::vector v_perf_fds(i_cpu_count, -1); + int i_prog_fd = bpf_program__fd(p_skel->progs.on_sample); + bool b_attached = false; + callback_ctx_t st_cb_ctx{&v_out_samples}; + struct ring_buffer* p_rb = nullptr; + + for (int i_cpu = 0; i_cpu < i_cpu_count; i_cpu++) { + v_perf_fds[i_cpu] = + perf_event_open(&st_attr, st_config.target_pid, i_cpu, -1, 0); + if (v_perf_fds[i_cpu] < 0) { + continue; + } + + if (ioctl(v_perf_fds[i_cpu], PERF_EVENT_IOC_SET_BPF, i_prog_fd) < 0) { + str_error_message = "PERF_EVENT_IOC_SET_BPF failed"; + goto cleanup; + } + + if (ioctl(v_perf_fds[i_cpu], PERF_EVENT_IOC_ENABLE, 0) < 0) { + str_error_message = "PERF_EVENT_IOC_ENABLE failed"; + goto cleanup; + } + + b_attached = true; + } + + if (!b_attached) { + str_error_message = "failed to open any perf event for target pid"; + goto cleanup; + } + + p_rb = ring_buffer__new(bpf_map__fd(p_skel->maps.rb), handle_event, + &st_cb_ctx, nullptr); + if (!p_rb) { + str_error_message = "failed to create ring buffer"; + goto cleanup; + } + + { + auto tp_start = std::chrono::steady_clock::now(); + while (!g_i_stop) { + ring_buffer__poll(p_rb, 100); + auto tp_now = std::chrono::steady_clock::now(); + auto i_elapsed_seconds = + std::chrono::duration_cast(tp_now - tp_start) + .count(); + if (i_elapsed_seconds >= st_config.duration_seconds) { + break; + } + } + } + + ring_buffer__free(p_rb); + prog_bpf__destroy(p_skel); + for (int i_fd : v_perf_fds) { + if (i_fd >= 0) { + close(i_fd); + } + } + return true; + +cleanup: + if (p_rb) { + ring_buffer__free(p_rb); + } + prog_bpf__destroy(p_skel); + for (int i_fd : v_perf_fds) { + if (i_fd >= 0) { + close(i_fd); + } + } + return false; +} + +} // namespace cpu_sampler