/*
 *
 * 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-2024 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 <cerrno>
#include <cmath>
#include <cstdio>
#include <mutex>
#include <string>
#include "../conky.h"
#include "../content/specials.h"
#include "../content/text_object.h"
#include "../core.h"
#include "../logging.h"
#include "../update-cb.hh"

struct exec_data {
  float interval{0};
  char *cmd{nullptr};
  size_t last_execp_hash{0};
  exec_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) {
      LOG_ERROR("failed to dup child fd: {}", strerror(errno));
    }
    close(childend);

    // Discard the command's stderr. We only capture stdout, so without this a
    // command that chatters on stderr every update interval (e.g. `sensors`
    // complaining about an unreadable subfeature) would inherit conky's stderr
    // and flood the terminal or system journal (#1596).
    int devnull = open("/dev/null", O_WRONLY);
    if (devnull != -1) {
      dup2(devnull, STDERR_FILENO);
      if (devnull != STDERR_FILENO) { close(devnull); }
    }

    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
 */
double get_barnum(const char *buf) {
  double barnum;

  /* The exec callback may not have produced output yet (e.g. when the
   * text object is newly created via conky_parse inside a Lua script).
   * Return 0.0 silently — this mirrors how print_exec/fill_p handles
   * the same transient condition (#1699). */
  if (buf == nullptr || buf[0] == '\0') { return 0.0; }

  if (sscanf(buf, "%lf", &barnum) != 1) {
    LOG_ERROR(
        "reading exec value failed (perhaps it's not the "
        "correct format?)");
    return 0.0;
  }
  if (barnum > 100.0 || barnum < 0.0) {
    LOG_WARNING(
        "exec value is not between 0 and 100, "
        "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) {
    size_t current_hash = std::hash<std::string>{}(buffer);

    auto *ed = static_cast<struct exec_data *>(obj->data.opaque);

    if (ed->last_execp_hash == 0 || current_hash != ed->last_execp_hash) {
      if (obj->sub) {
        free_text_objects(obj->sub);
        free(obj->sub);
      }

      obj->sub =
          static_cast<struct text_object *>(malloc(sizeof(struct text_object)));
      if (obj->sub) { memset(obj->sub, 0, sizeof(struct text_object)); }
      if (obj->sub) { extract_variable_text_internal(obj->sub, buffer); }

      ed->last_execp_hash = current_hash;
    }

    if (obj->sub) { generate_text_internal(p, p_max_size, *obj->sub); }
  } 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, exec_flag flags) {
  const char *cmd = arg;
  char *orig_cmd = nullptr;
  auto ed = new exec_data;

  /* in case we have an execi object, we need to parse out the interval */
  if (flags & exec_flag::interval) {
    int n;

    /* store the interval in ed->interval */
    if (sscanf(arg, "%f %n", &ed->interval, &n) <= 0) {
      LOG_ERROR("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 (flags & exec_flag::bar) {
    cmd = scan_bar(obj, cmd, 100);
#ifdef BUILD_GUI
  } else if (flags & exec_flag::gauge) {
    cmd = scan_gauge(obj, cmd, 100);
  } else if (flags & exec_flag::graph) {
    auto [buf, skip] = scan_command(cmd);
    scan_graph(obj, cmd + skip, 100, FALSE,
               buf != nullptr ? graph_data_key{fmt::format("exec:{}", buf)}
                              : graph_parent_obj_key);
    cmd = buf;
    if (cmd == nullptr) {
      LOG_ERROR("error parsing execgraph arguments: '{}'",
                arg ? arg : "(null)");
    }
#endif /* BUILD_GUI */
  }

  /* store the resulting command, or an empty string if something went wrong */
  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.
 *
 * For non-interval variants, ed->interval is 0 which yields period=1.
 * 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_exec(struct text_object *obj) {
  auto *ed = static_cast<struct exec_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 {
    LOG_DEBUG("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) {
  auto *ed = static_cast<struct exec_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;
}
