Exemplo n.º 1
0
static json_ref make_type_field(const struct watchman_rule_match* match) {
  // Bias towards the more common file types first
  if (S_ISREG(match->file->stat.mode)) {
    return typed_string_to_json("f", W_STRING_UNICODE);
  }
  if (S_ISDIR(match->file->stat.mode)) {
    return typed_string_to_json("d", W_STRING_UNICODE);
  }
  if (S_ISLNK(match->file->stat.mode)) {
    return typed_string_to_json("l", W_STRING_UNICODE);
  }
  if (S_ISBLK(match->file->stat.mode)) {
    return typed_string_to_json("b", W_STRING_UNICODE);
  }
  if (S_ISCHR(match->file->stat.mode)) {
    return typed_string_to_json("c", W_STRING_UNICODE);
  }
  if (S_ISFIFO(match->file->stat.mode)) {
    return typed_string_to_json("p", W_STRING_UNICODE);
  }
  if (S_ISSOCK(match->file->stat.mode)) {
    return typed_string_to_json("s", W_STRING_UNICODE);
  }
#ifdef S_ISDOOR
  if (S_ISDOOR(match->file->stat.mode)) {
    return typed_string_to_json("D", W_STRING_UNICODE);
  }
#endif
  return typed_string_to_json("?", W_STRING_UNICODE);
}
Exemplo n.º 2
0
/* get-sockname */
static void cmd_get_sockname(struct watchman_client* client, const json_ref&) {
  auto resp = make_response();

  resp.set("sockname", typed_string_to_json(get_sock_name(), W_STRING_BYTE));

  send_and_dispose_response(client, std::move(resp));
}
Exemplo n.º 3
0
void send_error_response(struct watchman_client *client,
    const char *fmt, ...)
{
  char buf[WATCHMAN_NAME_MAX];
  va_list ap;
  json_t *resp = make_response();
  json_t *errstr;

  va_start(ap, fmt);
  vsnprintf(buf, sizeof(buf), fmt, ap);
  va_end(ap);

  errstr = typed_string_to_json(buf, W_STRING_MIXED);
  set_prop(resp, "error", errstr);

  json_incref(errstr);
  w_perf_add_meta(&client->perf_sample, "error", errstr);

  if (client->current_command) {
    char *command = NULL;
    command = json_dumps(client->current_command, 0);
    w_log(W_LOG_ERR, "send_error_response: %s failed: %s\n",
        command, buf);
    free(command);
  } else {
    w_log(W_LOG_ERR, "send_error_response: %s\n", buf);
  }

  send_and_dispose_response(client, resp);
}
Exemplo n.º 4
0
/* unsubscribe /root subname
 * Cancels a subscription */
static void cmd_unsubscribe(
    struct watchman_client* clientbase,
    const json_ref& args) {
  const char *name;
  bool deleted{false};
  struct watchman_user_client *client =
      (struct watchman_user_client *)clientbase;

  auto root = resolve_root_or_err(client, args, 1, false);
  if (!root) {
    return;
  }

  auto jstr = json_array_get(args, 2);
  name = json_string_value(jstr);
  if (!name) {
    send_error_response(
        client, "expected 2nd parameter to be subscription name");
    return;
  }

  auto sname = json_to_w_string(jstr);
  deleted = client->unsubByName(sname);

  auto resp = make_response();
  resp.set({{"unsubscribe", typed_string_to_json(name)},
            {"deleted", json_boolean(deleted)}});

  send_and_dispose_response(client, std::move(resp));
}
Exemplo n.º 5
0
// Compute the effective value of the root_files configuration and
// return a json reference.  The caller must decref the ref when done
// (we may synthesize this value).   Sets enforcing to indicate whether
// we will only allow watches on the root_files.
// The array returned by this function (if not NULL) is guaranteed to
// list .watchmanconfig as its zeroth element.
json_ref cfg_compute_root_files(bool* enforcing) {
  *enforcing = false;

  json_ref ref = cfg_get_json("enforce_root_files");
  if (ref) {
    if (!ref.isBool()) {
      w_log(W_LOG_FATAL,
          "Expected config value enforce_root_files to be boolean\n");
    }
    *enforcing = ref.asBool();
  }

  ref = cfg_get_json("root_files");
  if (ref) {
    if (!is_array_of_strings(ref)) {
      w_log(W_LOG_FATAL,
          "global config root_files must be an array of strings\n");
      *enforcing = false;
      return nullptr;
    }
    prepend_watchmanconfig_to_array(ref);

    return ref;
  }

  // Try legacy root_restrict_files configuration
  ref = cfg_get_json("root_restrict_files");
  if (ref) {
    if (!is_array_of_strings(ref)) {
      w_log(W_LOG_FATAL, "deprecated global config root_restrict_files "
          "must be an array of strings\n");
      *enforcing = false;
      return nullptr;
    }
    prepend_watchmanconfig_to_array(ref);
    *enforcing = true;
    return ref;
  }

  // Synthesize our conservative default value.
  // .watchmanconfig MUST be first
  return json_array({typed_string_to_json(".watchmanconfig"),
                     typed_string_to_json(".hg"),
                     typed_string_to_json(".git"),
                     typed_string_to_json(".svn")});
}
Exemplo n.º 6
0
// Given an array of string values, if that array does not contain
// a ".watchmanconfig" entry, prepend it
static void prepend_watchmanconfig_to_array(json_ref& ref) {
  const char *val;

  if (json_array_size(ref) == 0) {
    // json_array_insert_new at index can fail when the array is empty,
    // so just append in this case.
    json_array_append_new(ref, typed_string_to_json(".watchmanconfig",
          W_STRING_UNICODE));
    return;
  }

  val = json_string_value(json_array_get(ref, 0));
  if (!strcmp(val, ".watchmanconfig")) {
    return;
  }
  json_array_insert_new(ref, 0, typed_string_to_json(".watchmanconfig",
        W_STRING_UNICODE));
}
Exemplo n.º 7
0
bool parse_field_list(
    json_ref field_list,
    w_query_field_list* selected,
    char** errmsg) {
  uint32_t i;

  selected->clear();

  if (!field_list) {
    // Use the default list
    field_list = json_array({typed_string_to_json("name", W_STRING_UNICODE),
                             typed_string_to_json("exists", W_STRING_UNICODE),
                             typed_string_to_json("new", W_STRING_UNICODE),
                             typed_string_to_json("size", W_STRING_UNICODE),
                             typed_string_to_json("mode", W_STRING_UNICODE)});
  }

  if (!json_is_array(field_list)) {
    *errmsg = strdup("field list must be an array of strings");
    return false;
  }

  for (i = 0; i < json_array_size(field_list); i++) {
    auto jname = json_array_get(field_list, i);

    if (!json_is_string(jname)) {
      *errmsg = strdup("field list must be an array of strings");
      return false;
    }

    auto name = json_to_w_string(jname);
    auto& defs = field_defs();
    auto it = defs.find(name);
    if (it == defs.end()) {
      ignore_result(asprintf(errmsg, "unknown field name '%s'", name.c_str()));
      return false;
    }
    selected->push_back(&it->second);
  }

  return true;
}
Exemplo n.º 8
0
static json_ref parse_value(lex_t *lex, size_t flags, json_error_t *error)
{
    json_ref json;

    switch(lex->token) {
        case TOKEN_STRING: {
            json = typed_string_to_json(lex->value.string, W_STRING_BYTE);
            break;
        }

        case TOKEN_INTEGER: {
            json = json_integer(lex->value.integer);
            break;
        }

        case TOKEN_REAL: {
            json = json_real(lex->value.real);
            break;
        }

        case TOKEN_TRUE:
            json = json_true();
            break;

        case TOKEN_FALSE:
            json = json_false();
            break;

        case TOKEN_NULL:
            json = json_null();
            break;

        case '{':
            json = parse_object(lex, flags, error);
            break;

        case '[':
            json = parse_array(lex, flags, error);
            break;

        case TOKEN_INVALID:
            error_set(error, lex, "invalid token");
            return nullptr;

        default:
            error_set(error, lex, "unexpected token");
            return nullptr;
    }

    if(!json)
        return nullptr;

    return json;
}
Exemplo n.º 9
0
void w_query_legacy_field_list(w_query_field_list* flist) {
  static const char *names[] = {
    "name", "exists", "size", "mode", "uid", "gid", "mtime",
    "ctime", "ino", "dev", "nlink", "new", "cclock", "oclock"
  };
  uint8_t i;
  auto list = json_array();

  for (i = 0; i < sizeof(names)/sizeof(names[0]); i++) {
    json_array_append_new(list, typed_string_to_json(names[i],
        W_STRING_UNICODE));
  }

  parse_field_list(list, flist);
}
Exemplo n.º 10
0
void add_root_warnings_to_response(json_t *response, w_root_t *root) {
  char *str = NULL;
  char *full = NULL;

  if (!root->last_recrawl_reason && !root->warning) {
    return;
  }

  if (cfg_get_bool(root, "suppress_recrawl_warnings", false)) {
    return;
  }

  if (root->last_recrawl_reason) {
    ignore_result(
        asprintf(&str, "Recrawled this watch %d times, most recently because:\n"
                       "%.*s\n"
                       "To resolve, please review the information on\n"
                       "%s#recrawl",
                 root->recrawl_count, root->last_recrawl_reason->len,
                 root->last_recrawl_reason->buf, cfg_get_trouble_url()));
  }

  ignore_result(asprintf(
      &full,
      "%.*s%s" // root->warning
      "%s\n"   // str (last recrawl reason)
      "To clear this warning, run:\n"
      "`watchman watch-del %.*s ; watchman watch-project %.*s`\n",
      root->warning ? root->warning->len : 0,
      root->warning ? root->warning->buf : "",
      root->warning && str ? "\n" : "", // newline if we have both strings
      str ? str : "", root->root_path->len, root->root_path->buf,
      root->root_path->len, root->root_path->buf));

  if (full) {
    set_prop(response, "warning", typed_string_to_json(full, W_STRING_MIXED));
  }
  free(str);
  free(full);
}
Exemplo n.º 11
0
static void cmd_debug_poison(
    struct watchman_client* client,
    const json_ref& args) {
  struct timeval now;

  auto root = resolve_root_or_err(client, args, 1, false);
  if (!root) {
    return;
  }

  gettimeofday(&now, NULL);

  set_poison_state(
      root->root_path,
      now,
      "debug-poison",
      std::error_code(ENOMEM, std::generic_category()));

  auto resp = make_response();
  resp.set("poison", typed_string_to_json(poisoned_reason, W_STRING_UNICODE));
  send_and_dispose_response(client, std::move(resp));
}
Exemplo n.º 12
0
static bool query_caps(
    json_ref& response,
    json_ref& result,
    const json_ref& arr,
    bool required) {
  size_t i;
  bool have_all = true;

  for (i = 0; i < json_array_size(arr); i++) {
    const auto& ele = arr.at(i);
    const char* capname = json_string_value(ele);
    bool have = w_capability_supported(json_to_w_string(ele));
    if (!have) {
      have_all = false;
    }
    if (!capname) {
      break;
    }
    result.set(capname, json_boolean(have));
    if (required && !have) {
      char *buf = NULL;
      ignore_result(asprintf(
          &buf,
          "client required capability `%s` is not supported by this server",
          capname));
      response.set("error", typed_string_to_json(buf, W_STRING_UNICODE));
      w_log(W_LOG_ERR, "version: %s\n", buf);
      free(buf);

      // Only trigger the error on the first one we hit.  Ideally
      // we'd tell the user about all of them, but it is a PITA to
      // join and print them here in C :-/
      required = false;
    }
  }
  return have_all;
}
Exemplo n.º 13
0
/* version */
static void cmd_version(struct watchman_client* client, const json_ref& args) {
  auto resp = make_response();

#ifdef WATCHMAN_BUILD_INFO
  resp.set(
      "buildinfo", typed_string_to_json(WATCHMAN_BUILD_INFO, W_STRING_UNICODE));
#endif

  /* ["version"]
   *    -> just returns the basic version information.
   * ["version", {"required": ["foo"], "optional": ["bar"]}]
   *    -> includes capability matching information
   */

  if (json_array_size(args) == 2) {
    const auto& arg_obj = args.at(1);

    auto req_cap = arg_obj.get_default("required");
    auto opt_cap = arg_obj.get_default("optional");

    auto cap_res = json_object_of_size(
        (opt_cap ? json_array_size(opt_cap) : 0) +
        (req_cap ? json_array_size(req_cap) : 0));

    if (opt_cap && opt_cap.isArray()) {
      query_caps(resp, cap_res, opt_cap, false);
    }
    if (req_cap && req_cap.isArray()) {
      query_caps(resp, cap_res, req_cap, true);
    }

    resp.set("capabilities", std::move(cap_res));
  }

  send_and_dispose_response(client, std::move(resp));
}
Exemplo n.º 14
0
// Translate from the legacy array into the new style, then
// delegate to the main parser.
// We build a big anyof expression
std::shared_ptr<w_query> w_query_parse_legacy(
    const std::shared_ptr<w_root_t>& root,
    const json_ref& args,
    int start,
    uint32_t* next_arg,
    const char* clockspec,
    json_ref* expr_p) {
  bool include = true;
  bool negated = false;
  uint32_t i;
  const char *term_name = "match";
  json_ref included, excluded;
  auto query_obj = json_object();

  if (!args.isArray()) {
    throw QueryParseError("Expected an array");
  }

  for (i = start; i < json_array_size(args); i++) {
    const char *arg = json_string_value(json_array_get(args, i));
    if (!arg) {
      /* not a string value! */
      throw QueryParseError(watchman::to<std::string>(
          "rule @ position ", i, " is not a string value"));
    }
  }

  for (i = start; i < json_array_size(args); i++) {
    const char *arg = json_string_value(json_array_get(args, i));
    if (!strcmp(arg, "--")) {
      i++;
      break;
    }
    if (!strcmp(arg, "-X")) {
      include = false;
      continue;
    }
    if (!strcmp(arg, "-I")) {
      include = true;
      continue;
    }
    if (!strcmp(arg, "!")) {
      negated = true;
      continue;
    }
    if (!strcmp(arg, "-P")) {
      term_name = "ipcre";
      continue;
    }
    if (!strcmp(arg, "-p")) {
      term_name = "pcre";
      continue;
    }

    // Which group are we going to file it into
    json_ref container;
    if (include) {
      if (!included) {
        included =
            json_array({typed_string_to_json("anyof", W_STRING_UNICODE)});
      }
      container = included;
    } else {
      if (!excluded) {
        excluded =
            json_array({typed_string_to_json("anyof", W_STRING_UNICODE)});
      }
      container = excluded;
    }

    auto term =
        json_array({typed_string_to_json(term_name, W_STRING_UNICODE),
                    typed_string_to_json(arg),
                    typed_string_to_json("wholename", W_STRING_UNICODE)});
    if (negated) {
      term = json_array({typed_string_to_json("not", W_STRING_UNICODE), term});
    }
    json_array_append_new(container, std::move(term));

    // Reset negated flag
    negated = false;
    term_name = "match";
  }

  if (excluded) {
    excluded =
        json_array({typed_string_to_json("not", W_STRING_UNICODE), excluded});
  }

  json_ref query_array;
  if (included && excluded) {
    query_array = json_array(
        {typed_string_to_json("allof", W_STRING_UNICODE), excluded, included});
  } else if (included) {
    query_array = included;
  } else {
    query_array = excluded;
  }

  // query_array may be NULL, which means find me all files.
  // Otherwise, it is the expression we want to use.
  if (query_array) {
    json_object_set_new_nocheck(
        query_obj, "expression", std::move(query_array));
  }

  // For trigger
  if (next_arg) {
    *next_arg = i;
  }

  if (clockspec) {
    json_object_set_new_nocheck(query_obj,
        "since", typed_string_to_json(clockspec, W_STRING_UNICODE));
  }

  /* compose the query with the field list */
  auto query = w_query_parse(root, query_obj);

  if (expr_p) {
    *expr_p = query_obj;
  }

  if (query) {
    w_query_legacy_field_list(&query->fieldList);
  }

  return query;
}
Exemplo n.º 15
0
void PerfLogThread::loop() {
  json_ref samples;
  char **envp;
  json_ref perf_cmd;
  int64_t sample_batch;

  w_set_thread_name("perflog");

  // Prep some things that we'll need each time we run a command
  {
    uint32_t env_size;
    auto envpht = w_envp_make_ht();
    char *statedir = dirname(strdup(watchman_state_file));
    w_envp_set_cstring(envpht, "WATCHMAN_STATE_DIR", statedir);
    w_envp_set_cstring(envpht, "WATCHMAN_SOCK", get_sock_name());
    envp = w_envp_make_from_ht(envpht, &env_size);
  }

  perf_cmd = cfg_get_json("perf_logger_command");
  if (json_is_string(perf_cmd)) {
    perf_cmd = json_array({perf_cmd});
  }
  if (!json_is_array(perf_cmd)) {
    w_log(
        W_LOG_FATAL,
        "perf_logger_command must be either a string or an array of strings\n");
  }

  sample_batch = cfg_get_int("perf_logger_command_max_samples_per_call", 4);

  while (!w_is_stopping()) {
    {
      auto wlock = samples_.wlock();
      if (!*wlock) {
        cond_.wait(wlock.getUniqueLock());
      }

      samples = nullptr;
      std::swap(samples, *wlock);
    }

    if (samples) {
      while (json_array_size(samples) > 0) {
        int i = 0;
        auto cmd = json_array();
        posix_spawnattr_t attr;
        posix_spawn_file_actions_t actions;
        pid_t pid;
        char **argv = NULL;

        json_array_extend(cmd, perf_cmd);

        while (i < sample_batch && json_array_size(samples) > 0) {
          char *stringy = json_dumps(json_array_get(samples, 0), 0);
          if (stringy) {
            json_array_append_new(
                cmd, typed_string_to_json(stringy, W_STRING_MIXED));
            free(stringy);
          }
          json_array_remove(samples, 0);
          i++;
        }

        argv = w_argv_copy_from_json(cmd, 0);
        if (!argv) {
          char *dumped = json_dumps(cmd, 0);
          w_log(W_LOG_FATAL, "error converting %s to an argv array\n", dumped);
        }

        posix_spawnattr_init(&attr);
#ifdef POSIX_SPAWN_CLOEXEC_DEFAULT
        posix_spawnattr_setflags(&attr, POSIX_SPAWN_CLOEXEC_DEFAULT);
#endif
        posix_spawn_file_actions_init(&actions);
        posix_spawn_file_actions_addopen(&actions, STDIN_FILENO, "/dev/null",
                                         O_RDONLY, 0666);
        posix_spawn_file_actions_addopen(&actions, STDOUT_FILENO, "/dev/null",
                                         O_WRONLY, 0666);
        posix_spawn_file_actions_addopen(&actions, STDERR_FILENO, "/dev/null",
                                         O_WRONLY, 0666);

        if (posix_spawnp(&pid, argv[0], &actions, &attr, argv, envp) == 0) {
          int status;
          while (waitpid(pid, &status, 0) != pid) {
            if (errno != EINTR) {
              break;
            }
          }
        } else {
          int err = errno;
          w_log(W_LOG_ERR, "failed to spawn %s: %s\n", argv[0],
                strerror(err));
        }

        posix_spawnattr_destroy(&attr);
        posix_spawn_file_actions_destroy(&actions);

        free(argv);
      }
    }
  }
}
Exemplo n.º 16
0
void watchman_perf_sample::log() {
  char *dumped = NULL;

  if (!will_log) {
    return;
  }

  // Assemble a perf blob
  auto info = json_object(
      {{"description", typed_string_to_json(description)},
       {"meta", meta_data},
       {"pid", json_integer(getpid())},
       {"version", typed_string_to_json(PACKAGE_VERSION, W_STRING_UNICODE)}});

#ifdef WATCHMAN_BUILD_INFO
  info.set(
      "buildinfo", typed_string_to_json(WATCHMAN_BUILD_INFO, W_STRING_UNICODE));
#endif

#define ADDTV(name, tv) info.set(name, json_real(w_timeval_abs_seconds(tv)))
  ADDTV("elapsed_time", duration);
  ADDTV("start_time", time_begin);
#ifdef HAVE_SYS_RESOURCE_H
  ADDTV("user_time", usage.ru_utime);
  ADDTV("system_time", usage.ru_stime);
#define ADDU(n) info.set(#n, json_integer(usage.n))
  ADDU(ru_maxrss);
  ADDU(ru_ixrss);
  ADDU(ru_idrss);
  ADDU(ru_minflt);
  ADDU(ru_majflt);
  ADDU(ru_nswap);
  ADDU(ru_inblock);
  ADDU(ru_oublock);
  ADDU(ru_msgsnd);
  ADDU(ru_msgrcv);
  ADDU(ru_nsignals);
  ADDU(ru_nvcsw);
  ADDU(ru_nivcsw);
#endif // HAVE_SYS_RESOURCE_H
#undef ADDU
#undef ADDTV

  // Log to the log file
  dumped = json_dumps(info, 0);
  w_log(W_LOG_ERR, "PERF: %s\n", dumped);
  free(dumped);

  if (!cfg_get_json("perf_logger_command")) {
    return;
  }

  // Send this to our logging thread for async processing

  {
    // The common case is that we already set up the logging
    // thread and that we can just log through it.
    auto rlock = perfThread.rlock();
    if (rlock->get()) {
      (*rlock)->addSample(std::move(info));
      return;
    }
  }

  // If it wasn't set, then we need an exclusive lock to
  // make sure that we don't spawn multiple instances.
  {
    auto wlock = perfThread.wlock();

    if (!wlock->get()) {
      *wlock = watchman::make_unique<PerfLogThread>();
    }

    (*wlock)->addSample(std::move(info));
  }
}
Exemplo n.º 17
0
static void *perf_log_thread(void *unused) {
  json_t *samples = NULL;
  char **envp;
  json_t *perf_cmd;
  int64_t sample_batch;

  unused_parameter(unused);

  w_set_thread_name("perflog");

  // Prep some things that we'll need each time we run a command
  {
    uint32_t env_size;
    w_ht_t *envpht = w_envp_make_ht();
    char *statedir = dirname(strdup(watchman_state_file));
    w_envp_set_cstring(envpht, "WATCHMAN_STATE_DIR", statedir);
    w_envp_set_cstring(envpht, "WATCHMAN_SOCK", get_sock_name());
    envp = w_envp_make_from_ht(envpht, &env_size);
  }

  perf_cmd = cfg_get_json(NULL, "perf_logger_command");
  if (json_is_string(perf_cmd)) {
    perf_cmd = json_pack("[O]", perf_cmd);
  }
  if (!json_is_array(perf_cmd)) {
    w_log(
        W_LOG_FATAL,
        "perf_logger_command must be either a string or an array of strings\n");
  }

  sample_batch =
      cfg_get_int(NULL, "perf_logger_command_max_samples_per_call", 4);

  while (true) {
    pthread_mutex_lock(&perf_log_lock);
    if (!perf_log_samples) {
      pthread_cond_wait(&perf_log_cond, &perf_log_lock);
    }
    samples = perf_log_samples;
    perf_log_samples = NULL;

    pthread_mutex_unlock(&perf_log_lock);

    if (samples) {
      while (json_array_size(samples) > 0) {
        int i = 0;
        json_t *cmd = json_array();
        posix_spawnattr_t attr;
        posix_spawn_file_actions_t actions;
        pid_t pid;
        char **argv = NULL;

        json_array_extend(cmd, perf_cmd);

        while (i < sample_batch && json_array_size(samples) > 0) {
          char *stringy = json_dumps(json_array_get(samples, 0), 0);
          json_array_append(cmd, typed_string_to_json(stringy, W_STRING_MIXED));
          free(stringy);
          json_array_remove(samples, 0);
          i++;
        }

        argv = w_argv_copy_from_json(cmd, 0);
        if (!argv) {
          char *dumped = json_dumps(cmd, 0);
          w_log(W_LOG_FATAL, "error converting %s to an argv array\n", dumped);
        }

        posix_spawnattr_init(&attr);
#ifdef POSIX_SPAWN_CLOEXEC_DEFAULT
        posix_spawnattr_setflags(&attr, POSIX_SPAWN_CLOEXEC_DEFAULT);
#endif
        posix_spawn_file_actions_init(&actions);
        posix_spawn_file_actions_addopen(&actions, STDIN_FILENO, "/dev/null",
                                         O_RDONLY, 0666);
        posix_spawn_file_actions_addopen(&actions, STDOUT_FILENO, "/dev/null",
                                         O_WRONLY, 0666);
        posix_spawn_file_actions_addopen(&actions, STDERR_FILENO, "/dev/null",
                                         O_WRONLY, 0666);

        if (posix_spawnp(&pid, argv[0], &actions, &attr, argv, envp) == 0) {
          // There's no sense waiting here, because w_reap_children is called
          // by the reaper thread.
        } else {
          int err = errno;
          w_log(W_LOG_ERR, "failed to spawn %s: %s\n", argv[0],
                strerror(err));
        }

        posix_spawnattr_destroy(&attr);
        posix_spawn_file_actions_destroy(&actions);

        free(argv);
        json_decref(cmd);
      }
      json_decref(samples);
    }
  }

  return NULL;
}