/*! * \file * Sequencer.cpp * \brief A basic sequence interpreter for snel * * Created on: Feb, 2019 * Author: Christos Choutouridis AEM: 8997 * email : */ #include "sequencer.h" namespace snel { //! A type-safe memcpy template void memcpy (T* to, const T* from, size_t n) { for (size_t i=0 ; i void split(const std::basic_string& str, Container& cont, T delim) { std::basic_stringstream ss(str); std::basic_string token; while (std::getline(ss, token, delim)) { cont.push_back(token); } } /*! * A very very simple filtering for leading ' ', comments and adjustments for * the '|' character. * \note * The current snel implementation requires '|' to separated * from left and right from the rest of the text, so we adjust the input to fit * this expectation. * \param in Input string * \return Output string */ std::string filter (const std::string in) { auto first = in.find_first_not_of(' '); std::string nonws = (first != std::string::npos) ? in.substr(first) : ""; std::string out{}; char prev {0}, cur, next {0}; // return empty if the first non-whitespace character is '#' if (nonws[0] == '#') return out; for (auto i = nonws.begin(); i != nonws.end() ; ++i) { cur = *i; if (cur == '|') { if (prev != ' ') out += ' '; // prev| insert a space before next = *++i; if (next == '|' || next == ' ') { out += cur; cur = next; // syntax ok } else { out += cur; out += ' '; // |next, insert a space after cur = next; } } prev = cur; out += cur; } return out; } /* * ========== ArgList ========== */ /*! * destructor to free up all the allocations * \note * Using RAII for ArgList we ensure "no memory leak" */ ArgList::~ArgList() { for (vtype p : args_) if (p != nullptr) delete[] p; } /*! * A push_back wrapper to our underling vector. This ensures NULL terminating * argument list for exec() sys-call. * \param item The argument to push * \return Reference to ArgList for chaining */ ArgList& ArgList::push_back(const std::string& item) { vtype it = new type[item.length()+1]; memcpy (it, item.c_str(), item.length()); // get data and null terminate them it[item.length()] = 0; args_.back() = it; // update the argument vector args_.push_back(nullptr); return *this; } /* * ========== Pipe =========== */ //! Destructor to free up all the resources Pipe::~Pipe() { // close any possibly leaked pipes if (fd[0] != -1) close(fd[0]); if (fd[1] != -1) close(fd[1]); } /* * ========== Child ========== */ //! Destructor to free up all the resources Child::~Child () { for (int i=0 ; i<3 ; ++i) restore_std_if(i); } /*! * Redirect a \p std_fd file descriptor to a file * @param fn The file name to used as std * @param std_fd The file descriptor to redirect */ void Child::redirect_std_if(std::string fn, fd_t std_fd) { if (fn != "") { if ((files[std_fd] = openat(AT_FDCWD, fn.c_str(), O_RDWR | O_CREAT, 0640)) == -1) throw Error ("Child: Can not open file"); if ((sstd[std_fd] = dup (std_fd)) == -1) // save STDxxx throw Error ("Child: Can not create file descriptor"); if ((dup2(files[std_fd], std_fd)) == -1) // use input as STDIN_ throw Error ("Child: Can not redirect file descriptor"); } } /*! * Restore and redirected std file descriptor to its original location * \param std_fd The file descriptor to restore */ void Child::restore_std_if(fd_t std_fd) { if (sstd[std_fd] != -1) { dup2(sstd[std_fd], std_fd); close (sstd[std_fd]); sstd[std_fd] = -1; } if (files[std_fd] != -1) { close (files[std_fd]); files[std_fd] = -1; } } /*! * Parse the command of the current child to produce the actual arguments * \return Reference to Child for chaining */ Child& Child::make_arguments () { bool in{false}, out{false}, err{false}; bool CanRedirectIn = (!pipe_.from) ? true : false; bool CanRedirectOut = (!pipe_.to) ? true : false; for (auto& t: command) { if (t == "" || t== " " || t=="|") continue; // skip crap if (t == "&&") logic = LogicOp::AND; else if (t == "||") logic = LogicOp::OR; // one pass redirection parsing else if (CanRedirectIn && !t.compare(0, 1, "<")) filenames[STDIN_] = t.substr(1); else if (CanRedirectOut&& !t.compare(0, 1, ">")) filenames[STDOUT_] = t.substr(1); else if (CanRedirectOut&& !t.compare(0, 2, "1>")) filenames[STDOUT_] = t.substr(2); else if (!t.compare(0, 2, "2>")) filenames[STDERR_] = t.substr(2); // two pass redirection parsing (if redirection came in 2 arguments) else if (t == "<") in = true; else if (t == "1>" || t == ">") out = true; else if (t == "2>") err = true; else if (in) { if (CanRedirectIn) filenames[STDIN_] = t; in = false; } else if (out) { if (CanRedirectOut) filenames[STDOUT_] = t; out = false; } else if (err) { filenames[STDERR_] = t; err = false; } else arguments.push_back(t); } return *this; } /*! * \brief Execute the child in separate process. * * This is the hart of the snel. We use fork() - execvp() pair to execute * each child. In case of std redirection, the fd/dup2 handling is made in the * parent. In case of piping, the fd handling is made in parent and the * dup2() calls in the child process. * * A child is a member of a vector containing the command chain. * For ex: * \code ls && cat tralala || uname -a \endcode * is a chain with three childs (ls, cat, uname). \see Sequencer * * \param it Iterator to the command chain execution * \param first flag to indicate the first command in the chain * \return True if the chain can stop. */ bool Child::execute(std::vector::iterator it, bool first) { bool stop {false}; Child& previous = (!first) ? *--it : *it; // get previous child safely // Check redirection requirements redirect_std_if (filenames[STDIN_], STDIN_); redirect_std_if (filenames[STDOUT_], STDOUT_); redirect_std_if (filenames[STDERR_], STDERR_); // Check parent pipe control if (pipe_.to) { if((::pipe (pipe_.fd)) == -1) throw Error("Child: Can not create pipe"); } pid_t pid = fork(); if (pid < 0) { throw Error("Child: Can not create child process"); } else if (pid == 0) { // =========== child ================ // Some extra pipe checking while we still have our data if (pipe_.to) { // transmitting child close(pipe_.fd[0]); // close the read end (of the child copy) if ((dup2(pipe_.fd[1], STDOUT_)) == -1) // redirect output throw Error("Child pipe[to]: Can not redirect through pipe"); } else if (!first && pipe_.from) { // receiving child close (previous.pipe().fd[1]); // close the write end (of the child copy) if ((dup2(previous.pipe().fd[0], STDIN_)) == -1) // redirect input throw Error("Child pipe[from]: Can not redirect through pipe"); } execvp(arguments.front(), arguments.data()); // error if we got here std::cout << "Can not invoke: " << arguments.front() << std::endl; throw Error("Child: Can not run child process"); } else { // =========== parent ================ if (pipe_.to) close (pipe_.fd[1]); // Pipe fd control else if (!first && pipe_.from) close (previous.pipe().fd[0]); int exit_status; waitpid(pid, &exit_status, 0); // wait child process to finish restore_std_if (STDIN_); // restore all redirections restore_std_if (STDOUT_); restore_std_if (STDERR_); // Check if we don't have to execute the rest of the chain if ( exit_status && logic == LogicOp::AND) stop = true; if (!exit_status && logic == LogicOp::OR) stop = true; } return stop; } /* * ======== Sequencer =========== */ /*! * First parsing of each line. We split input in ';' to create * command chains and split each chain further to create child tokens. * These token will be processed further by the child's `make_arguments()` * * \param input The input string to parse * \return Reference to Sequencer for chaining */ Sequencer& Sequencer::parse (const std::string& input) { std::vector chains; // chains are vector of commands std::vector tokens; // token is any ' ' separated string Child::command_t command; split (input, chains, ';'); // split input to command chains using ';' for (auto& chain : chains) { // for each chain command.clear(); // clear child tokens tokens.clear(); // make a new token vector for command split (chain, tokens, ' '); // split chain in to tokens using ' ' seq_.emplace_back(std::vector{}); // make a new child for command // check tokens inside the chain bool from = false; for (auto& t: tokens) { command.push_back(t); // get current token if (is_seperator(t)) { // construct a child with what we have so far and clear command seq_.back().emplace_back(command); command.clear(); if (from) { // set from flag to link with the previous child seq_.back().back().pipe().from = from; from = false; } if (is_pipe(t)) { seq_.back().back().pipe().to = true; // mark as transmitting child from = true; // mark to inform the next child as receiving } } } seq_.back().emplace_back(command); // construct the child with tokens if (from) seq_.back().back().pipe().from = from; } return *this; } /*! * The main sequencer execution. We recurse for each command chain * and each child in the chain and we parse-execute the child * \return Reference to Sequence for chaining */ Sequencer& Sequencer::execute() { bool first; for (auto& chain : seq_) { // loop in all of the command chains first = true; for (auto it = chain.begin() ; it != chain.end(); ++it) { // loop all of the commands in the chain Child& command = *it; if (command.make_arguments().execute(it, first)) break; // break the chain if child's logic operator says so. first = false; } } seq_.clear(); // clear the sequencer's data for the next line (call all the destructors) return *this; } }