conky/src/exec.cc

389 lines
11 KiB
C++

/*
*
* Conky, a system monitor, based on torsmo
*
* Any original torsmo code is licensed under the BSD license
*
* All code written since the fork of torsmo is licensed under the GPL
*
* Please see COPYING for details
*
* Copyright (c) 2004, Hannu Saransaari and Lauri Hakkarainen
* Copyright (c) 2005-2021 Brenden Matthews, Philip Kovacs, et. al.
* (see AUTHORS)
* All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "exec.h"
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <cmath>
#include <cstdio>
#include <mutex>
#include "conky.h"
#include "core.h"
#include "logging.h"
#include "specials.h"
#include "text_object.h"
#include "update-cb.hh"
struct execi_data {
float interval{0};
char *cmd{nullptr};
execi_data() = default;
};
static const int cmd_len = 256;
static char cmd[cmd_len];
static char *remove_excess_quotes(const char *);
static char *remove_excess_quotes(const char *command) {
char *cmd_ptr = cmd;
const char *command_ptr = command;
int skip = 0;
if ((cmd_len - 1) < (strlen(command) - 1)) {
snprintf(cmd, cmd_len - 1, "%s", command);
return cmd;
}
if (*command_ptr == '"' || *command_ptr == '\'') {
skip = 1;
command_ptr++;
}
for (; *command_ptr; command_ptr++) {
if ('\0' == *(command_ptr + 1) && 1 == skip &&
(*command_ptr == '"' || *command_ptr == '\'')) {
continue;
}
*cmd_ptr++ = *command_ptr;
}
*cmd_ptr = '\0';
return cmd;
}
// our own implementation of popen, the difference : the value of 'childpid'
// will be filled with the pid of the running 'command'. This is useful if want
// to kill it when it hangs while reading or writing to it. We have to kill it
// because pclose will wait until the process dies by itself
static FILE *pid_popen(const char *command, const char *mode, pid_t *child) {
int ends[2];
int parentend, childend;
// by running pipe after the strcmp's we make sure that we don't have to
// create a pipe and close the ends if mode is something illegal
if (strcmp(mode, "r") == 0) {
if (pipe(ends) != 0) { return nullptr; }
parentend = ends[0];
childend = ends[1];
} else if (strcmp(mode, "w") == 0) {
if (pipe(ends) != 0) { return nullptr; }
parentend = ends[1];
childend = ends[0];
} else {
return nullptr;
}
*child = fork();
if (*child == -1) {
close(parentend);
close(childend);
return nullptr;
}
if (*child > 0) {
close(childend);
waitpid(*child, nullptr, 0);
} else {
// don't read from both stdin and pipe or write to both stdout and pipe
if (childend == ends[0]) {
close(0);
} else {
close(1);
}
close(parentend);
// by dupping childend, the returned fd will have close-on-exec turned off
if (fcntl(childend, F_DUPFD, 0) == -1) { perror("fcntl()"); }
close(childend);
execl("/bin/sh", "sh", "-c", remove_excess_quotes(command),
(char *)nullptr);
_exit(EXIT_FAILURE); // child should die here, (normally execl will take
// care of this but it can fail)
}
return fdopen(parentend, mode);
}
/**
* Executes a command and stores the result
*
* This function is called automatically, either once every update
* interval, or at specific intervals in the case of execi commands.
* conky::run_all_callbacks() handles this. In order for this magic to
* happen, we must register a callback with conky::register_cb<exec_cb>()
* and store it somewhere, such as obj->exec_handle. To retrieve the
* results, use the stored callback to call get_result_copy(), which
* returns a std::string.
*/
void exec_cb::work() {
pid_t childpid;
std::string buf;
std::shared_ptr<FILE> fp;
char b[0x1000];
if (FILE *t = pid_popen(std::get<0>(tuple).c_str(), "r", &childpid)) {
fp.reset(t, fclose);
} else {
return;
}
while ((feof(fp.get()) == 0) && (ferror(fp.get()) == 0)) {
int length = fread(b, 1, sizeof b, fp.get());
buf.append(b, length);
}
if (*buf.rbegin() == '\n') { buf.resize(buf.size() - 1); }
std::lock_guard<std::mutex> l(result_mutex);
result = buf;
}
// remove backspaced chars, example: "dog^H^H^Hcat" becomes "cat"
// string has to end with \0 and it's length should fit in a int
#define BACKSPACE 8
static void remove_deleted_chars(char *string, unsigned int p_max_size) {
int i = 0;
while (string[i] != 0) {
if (string[i] == BACKSPACE) {
if (i != 0) {
strncpy(&(string[i - 1]), &(string[i + 1]),
strnlen(string, p_max_size) - i + 1);
i--;
} else {
strncpy(&(string[i]), &(string[i + 1]),
strnlen(string, p_max_size) -
i); // necessary for ^H's at the start of a string
}
} else {
i++;
}
}
}
/**
* Parses command output to find a number between 0.0 and 100.0.
* Used by ${exec[i]{bar,gauge,graph}}.
*
* @param[in] buf output of a command executed by an exec_cb object
* @return number between 0.0 and 100.0
*/
static inline double get_barnum(const char *buf) {
double barnum;
if (sscanf(buf, "%lf", &barnum) != 1) {
NORM_ERR(
"reading exec value failed (perhaps it's not the "
"correct format?)");
return 0.0;
}
if (barnum > 100.0 || barnum < 0.0) {
NORM_ERR(
"your exec value is not between 0 and 100, "
"therefore it will be ignored");
return 0.0;
}
return barnum;
}
/**
* Store command output in p. For execp objects, we process the output
* in case it contains special commands like ${color}
*
* @param[in] buffer the output of a command
* @param[in] obj text_object that specifies whether or not to parse
* @param[out] p the string in which we store command output
* @param[in] p_max_size the maximum size of p...
*/
void fill_p(const char *buffer, struct text_object *obj, char *p,
unsigned int p_max_size) {
if (obj->parse) {
evaluate(buffer, p, p_max_size);
} else {
snprintf(p, p_max_size, "%s", buffer);
}
remove_deleted_chars(p, p_max_size);
}
/**
* Parses arg to find the command to be run, as well as special options
* like height, width, color, and update interval
*
* @param[out] obj stores the command and an execi_data structure (if
* applicable)
* @param[in] arg the argument to an ${exec*} object
* @param[in] execflag bitwise flag used to specify the exec variant we need to
* process
*/
void scan_exec_arg(struct text_object *obj, const char *arg,
unsigned int execflag) {
const char *cmd = arg;
char *orig_cmd = nullptr;
struct execi_data *ed;
/* in case we have an execi object, we need to parse out the interval */
if ((execflag & EF_EXECI) != 0u) {
ed = new execi_data;
int n;
/* store the interval in ed->interval */
if (sscanf(arg, "%f %n", &ed->interval, &n) <= 0) {
NORM_ERR("missing execi interval: ${execi* <interval> command}");
delete ed;
ed = nullptr;
return;
}
/* set cmd to everything after the interval */
cmd = strndup(arg + n, text_buffer_size.get(*state));
orig_cmd = const_cast<char *>(cmd);
}
/* parse any special options for the graphical exec types */
if ((execflag & EF_BAR) != 0u) {
cmd = scan_bar(obj, cmd, 100);
#ifdef BUILD_GUI
} else if ((execflag & EF_GAUGE) != 0u) {
cmd = scan_gauge(obj, cmd, 100);
} else if ((execflag & EF_GRAPH) != 0u) {
auto [buf, skip] = scan_command(cmd);
scan_graph(obj, cmd + skip, 100);
cmd = buf;
if (cmd == nullptr) {
NORM_ERR("error parsing arguments to execgraph object");
}
#endif /* BUILD_GUI */
}
/* finally, store the resulting command, or an empty string if something went
* wrong */
if ((execflag & EF_EXEC) != 0u) {
obj->data.s =
strndup(cmd != nullptr ? cmd : "", text_buffer_size.get(*state));
} else if ((execflag & EF_EXECI) != 0u) {
ed->cmd = strndup(cmd != nullptr ? cmd : "", text_buffer_size.get(*state));
obj->data.opaque = ed;
}
free_and_zero(orig_cmd);
}
/**
* Register an exec_cb object using the command that we have parsed
*
* @param[out] obj stores the callback handle
*/
void register_exec(struct text_object *obj) {
if ((obj->data.s != nullptr) && (obj->data.s[0] != 0)) {
obj->exec_handle = new conky::callback_handle<exec_cb>(
conky::register_cb<exec_cb>(1, true, obj->data.s));
} else {
DBGP("unable to register exec callback");
}
}
/**
* Register an exec_cb object using the command that we have parsed.
*
* This version takes care of execi intervals. Note that we depend on
* obj->thread, so be sure to run this function *after* setting obj->thread.
*
* @param[out] obj stores the callback handle
*/
void register_execi(struct text_object *obj) {
auto *ed = static_cast<struct execi_data *>(obj->data.opaque);
if ((ed != nullptr) && (ed->cmd != nullptr) && (ed->cmd[0] != 0)) {
uint32_t period =
std::max(lround(ed->interval / active_update_interval()), 1l);
obj->exec_handle = new conky::callback_handle<exec_cb>(
conky::register_cb<exec_cb>(period, !obj->thread, ed->cmd));
} else {
DBGP("unable to register execi callback");
}
}
/**
* Get the results of an exec_cb object (command output)
*
* @param[in] obj holds an exec_handle, assuming one was registered
* @param[out] p the string in which we store command output
* @param[in] p_max_size the maximum size of p...
*/
void print_exec(struct text_object *obj, char *p, unsigned int p_max_size) {
if (obj->exec_handle != nullptr) {
fill_p((*obj->exec_handle)->get_result_copy().c_str(), obj, p, p_max_size);
}
}
/**
* Get the results of a graphical (bar, gauge, graph) exec_cb object
*
* @param[in] obj hold an exec_handle, assuming one was registered
* @return a value between 0.0 and 100.0
*/
double execbarval(struct text_object *obj) {
if (obj->exec_handle != nullptr) {
return get_barnum((*obj->exec_handle)->get_result_copy().c_str());
}
return 0.0;
}
/**
* Free up any dynamically allocated data
*
* @param[in] obj holds the data that we need to free up
*/
void free_exec(struct text_object *obj) {
free_and_zero(obj->data.s);
delete obj->exec_handle;
obj->exec_handle = nullptr;
}
/**
* Free up any dynamically allocated data, specifically for execi objects
*
* @param[in] obj holds the data that we need to free up
*/
void free_execi(struct text_object *obj) {
auto *ed = static_cast<struct execi_data *>(obj->data.opaque);
/* if ed is nullptr, there is nothing to do */
if (ed == nullptr) { return; }
delete obj->exec_handle;
obj->exec_handle = nullptr;
free_and_zero(ed->cmd);
delete ed;
ed = nullptr;
obj->data.opaque = nullptr;
}