Exemple #1
0
/* parse an expression term. It can be one of:
 * "term"
 * ["term" <parameters>]
 */
w_query_expr *w_query_expr_parse(w_query *query, json_t *exp)
{
  w_string_t *name;
  w_query_expr_parser parser;

  if (json_is_string(exp)) {
    name = w_string_new(json_string_value(exp));
  } else if (json_is_array(exp) && json_array_size(exp) > 0) {
    json_t *first = json_array_get(exp, 0);

    if (!json_is_string(first)) {
      query->errmsg = strdup(
          "first element of an expression must be a string");
      return NULL;
    }
    name = w_string_new(json_string_value(first));
  } else {
    query->errmsg = strdup("expected array or string for an expression");
    return NULL;
  }

  parser = w_ht_val_ptr(w_ht_get(term_hash, w_ht_ptr_val(name)));

  if (!parser) {
    ignore_result(asprintf(&query->errmsg,
        "unknown expression term '%s'",
        name->buf));
    w_string_delref(name);
    return NULL;
  }
  w_string_delref(name);
  return parser(query, exp);
}
Exemple #2
0
static bool parse_relative_root(w_root_t *root, w_query *res, json_t *query)
{
  json_t *relative_root;
  w_string_t *path, *canon_path;

  relative_root = json_object_get(query, "relative_root");
  if (!relative_root) {
    return true;
  }

  if (!json_is_string(relative_root)) {
    res->errmsg = strdup("'relative_root' must be a string");
    return false;
  }

  path = w_string_new(json_string_value(relative_root));
  canon_path = w_string_canon_path(path);
  res->relative_root = w_string_path_cat(root->root_path, canon_path);
  res->relative_root_slash = w_string_make_printf("%.*s%c",
        res->relative_root->len, res->relative_root->buf,
        WATCHMAN_DIR_SEP);
  w_string_delref(path);
  w_string_delref(canon_path);

  return true;
}
Exemple #3
0
static bool kqueue_root_start_watch_file(watchman_global_watcher_t watcher,
      w_root_t *root, struct watchman_file *file) {
  struct kqueue_root_state *state = root->watch;
  struct kevent k;
  w_ht_val_t fdval;
  int fd;
  w_string_t *full_name;
  unused_parameter(watcher);

  full_name = w_string_path_cat(file->parent->path, file->name);
  pthread_mutex_lock(&state->lock);
  if (w_ht_lookup(state->name_to_fd, w_ht_ptr_val(full_name), &fdval, false)) {
    // Already watching it
    pthread_mutex_unlock(&state->lock);
    return true;
  }
  pthread_mutex_unlock(&state->lock);

  w_log(W_LOG_DBG, "watch_file(%s)\n", full_name->buf);

  fd = open(full_name->buf, O_EVTONLY|O_CLOEXEC);
  if (fd == -1) {
    w_log(W_LOG_ERR, "failed to open %s O_EVTONLY: %s\n",
        full_name->buf, strerror(errno));
    w_string_delref(full_name);
    return false;
  }

  memset(&k, 0, sizeof(k));
  EV_SET(&k, fd, EVFILT_VNODE, EV_ADD|EV_CLEAR,
      NOTE_WRITE|NOTE_DELETE|NOTE_EXTEND|NOTE_RENAME|NOTE_ATTRIB,
      0, full_name);

  pthread_mutex_lock(&state->lock);
  w_ht_replace(state->name_to_fd, w_ht_ptr_val(full_name), fd);
  w_ht_replace(state->fd_to_name, fd, w_ht_ptr_val(full_name));
  pthread_mutex_unlock(&state->lock);

  if (kevent(state->kq_fd, &k, 1, NULL, 0, 0)) {
    w_log(W_LOG_DBG, "kevent EV_ADD file %s failed: %s",
        full_name->buf, strerror(errno));
    close(fd);
    pthread_mutex_lock(&state->lock);
    w_ht_del(state->name_to_fd, w_ht_ptr_val(full_name));
    w_ht_del(state->fd_to_name, fd);
    pthread_mutex_unlock(&state->lock);
  } else {
    w_log(W_LOG_DBG, "kevent file %s -> %d\n", full_name->buf, fd);
  }
  w_string_delref(full_name);

  return true;
}
Exemple #4
0
bool dispatch_command(struct watchman_client *client, json_t *args)
{
  watchman_command_func func;
  const char *cmd_name;
  w_string_t *cmd;

  if (!json_array_size(args)) {
    send_error_response(client,
        "invalid command (expected an array with some elements!)");
    return false;
  }

  cmd_name = json_string_value(json_array_get(args, 0));
  if (!cmd_name) {
    send_error_response(client,
        "invalid command: expected element 0 to be the command name");
    return false;
  }
  cmd = w_string_new(cmd_name);
  func = (watchman_command_func)w_ht_get(command_funcs, (w_ht_val_t)cmd);
  w_string_delref(cmd);

  if (func) {
    func(client, args);
    return true;
  }
  send_error_response(client, "unknown command %s", cmd_name);

  return false;
}
Exemple #5
0
bool w_capability_supported(const char *name) {
  bool res;
  w_string_t *namestr = w_string_new(name);
  res = w_ht_get(capabilities, w_ht_ptr_val(namestr));
  w_string_delref(namestr);
  return res;
}
Exemple #6
0
static bool winwatch_root_consume_notify(watchman_global_watcher_t watcher,
    w_root_t *root, struct watchman_pending_collection *coll)
{
  struct winwatch_root_state *state = root->watch;
  struct winwatch_changed_item *head, *item;
  struct timeval now;
  int n = 0;
  unused_parameter(watcher);

  pthread_mutex_lock(&state->mtx);
  head = state->head;
  state->head = NULL;
  state->tail = NULL;
  pthread_mutex_unlock(&state->mtx);

  gettimeofday(&now, NULL);

  while (head) {
    item = head;
    head = head->next;
    n++;

    w_log(W_LOG_DBG, "readchanges: add pending %.*s\n",
        item->name->len, item->name->buf);
    w_pending_coll_add(coll, item->name, now, W_PENDING_VIA_NOTIFY);

    w_string_delref(item->name);
    free(item);
  }

  return n > 0;
}
Exemple #7
0
void fsevents_root_dtor(watchman_global_watcher_t watcher, w_root_t *root) {
  struct fsevents_root_state *state = root->watch;
  unused_parameter(watcher);

  if (!state) {
    return;
  }

  // wait for fsevents thread to quit
  if (!pthread_equal(state->fse_thread, pthread_self())) {
    void *ignore;
    pthread_join(state->fse_thread, &ignore);
  }

  pthread_cond_destroy(&state->fse_cond);
  pthread_mutex_destroy(&state->fse_mtx);
  close(state->fse_pipe[0]);
  close(state->fse_pipe[1]);

  while (state->fse_head) {
    struct watchman_fsevent *evt = state->fse_head;
    state->fse_head = evt->next;

    w_string_delref(evt->path);
    free(evt);
  }

  free(state);
  root->watch = NULL;
}
Exemple #8
0
void w_query_delref(w_query *query)
{
  uint32_t i;

  if (!w_refcnt_del(&query->refcnt)) {
    return;
  }

  for (i = 0; i < query->npaths; i++) {
    if (query->paths[i].name) {
      w_string_delref(query->paths[i].name);
    }
  }
  free(query->paths);

  if (query->since_spec) {
    w_clockspec_free(query->since_spec);
  }

  if (query->expr) {
    w_query_expr_delref(query->expr);
  }

  free(query);
}
Exemple #9
0
w_string_t *w_query_ctx_get_wholename(
    struct w_query_ctx *ctx
)
{
  w_string_t *full_name;
  uint32_t name_start;

  if (ctx->wholename) {
    return ctx->wholename;
  }

  if (ctx->query->relative_root != NULL) {
    // At this point every path should start with the relative root, so this is
    // legal
    name_start = ctx->query->relative_root->len + 1;
  } else {
    name_start = ctx->root->root_path->len + 1;
  }

  full_name = w_string_path_cat(compute_parent_path(ctx, ctx->file),
                                w_file_get_name(ctx->file));

  // Record the name relative to the root
  ctx->wholename = w_string_slice(full_name, name_start,
      full_name->len - name_start);
  w_string_delref(full_name);

  return ctx->wholename;
}
Exemple #10
0
void w_clockspec_free(struct w_clockspec *spec)
{
  if (spec->tag == w_cs_named_cursor) {
    w_string_delref(spec->named_cursor.cursor);
  }
  free(spec);
}
Exemple #11
0
static bool portfs_root_consume_notify(watchman_global_watcher_t watcher,
    w_root_t *root, struct watchman_pending_collection *coll)
{
  struct portfs_root_state *state = root->watch;
  uint_t i, n;
  struct timeval now;
  unused_parameter(watcher);

  errno = 0;

  n = 1;
  if (port_getn(state->port_fd, state->portevents,
        sizeof(state->portevents) / sizeof(state->portevents[0]), &n, NULL)) {
    if (errno == EINTR) {
      return false;
    }
    w_log(W_LOG_FATAL, "port_getn: %s\n",
        strerror(errno));
  }

  w_log(W_LOG_DBG, "port_getn: n=%u\n", n);

  if (n == 0) {
    return false;
  }

  for (i = 0; i < n; i++) {
    if (IS_DIR_BIT_SET(state->portevents[i].portev_user)) {
      struct watchman_dir *dir = DECODE_DIR(state->portevents[i].portev_user);
      uint32_t pe = state->portevents[i].portev_events;

      w_log(W_LOG_DBG, "port: dir %.*s [0x%x]\n",
          dir->path->len, dir->path->buf, pe);

      if ((pe & (FILE_RENAME_FROM|UNMOUNTED|MOUNTEDOVER|FILE_DELETE))
          && w_string_equal(dir->path, root->root_path)) {

        w_log(W_LOG_ERR,
          "root dir %s has been (re)moved (code 0x%x), canceling watch\n",
          root->root_path->buf, pe);

        w_root_cancel(root);
        return false;
      }
      w_pending_coll_add(coll, dir->path, false, now, true);

    } else {
      struct watchman_file *file = state->portevents[i].portev_user;
      w_string_t *path;

      path = w_string_path_cat(file->parent->path, file->name);
      w_pending_coll_add(coll, path, true, now, true);
      w_log(W_LOG_DBG, "port: file %.*s\n", path->len, path->buf);
      w_string_delref(path);
    }
  }

  return true;
}
Exemple #12
0
static void dispose_dirname(void *ptr) {
    struct dirname_data *data = ptr;

    if (data->dirname) {
        w_string_delref(data->dirname);
    }
    free(data);
}
Exemple #13
0
static void delete_subscription(w_ht_val_t val)
{
  struct watchman_client_subscription *sub = w_ht_val_ptr(val);

  w_string_delref(sub->name);
  w_query_delref(sub->query);
  free(sub);
}
Exemple #14
0
void w_perf_destroy(w_perf_t *perf) {
  if (perf->root_path) {
    w_string_delref(perf->root_path);
  }
  if (perf->meta_data) {
    json_decref(perf->meta_data);
  }
  memset(perf, 0, sizeof(*perf));
}
Exemple #15
0
void w_match_results_free(uint32_t num_matches,
    struct watchman_rule_match *matches)
{
  uint32_t i;

  for (i = 0; i < num_matches; i++) {
    w_string_delref(matches[i].relname);
  }
  free(matches);
}
Exemple #16
0
static bool kqueue_root_consume_notify(watchman_global_watcher_t watcher,
    w_root_t *root, struct watchman_pending_collection *coll)
{
  struct kqueue_root_state *state = root->watch;
  int n;
  int i;
  struct timespec ts = { 0, 0 };
  struct timeval now;
  unused_parameter(watcher);

  errno = 0;

  w_log(W_LOG_DBG, "kqueue(%s)\n", root->root_path->buf);
  n = kevent(state->kq_fd, NULL, 0,
      state->keventbuf, sizeof(state->keventbuf) / sizeof(state->keventbuf[0]),
      &ts);
  w_log(W_LOG_DBG, "consume_kqueue: %s n=%d err=%s\n",
      root->root_path->buf, n, strerror(errno));
  if (root->cancelled) {
    return 0;
  }

  gettimeofday(&now, NULL);
  for (i = 0; n > 0 && i < n; i++) {
    uint32_t fflags = state->keventbuf[i].fflags;

    if (IS_DIR_BIT_SET(state->keventbuf[i].udata)) {
      struct watchman_dir *dir = DECODE_DIR(state->keventbuf[i].udata);

      w_log(W_LOG_DBG, " KQ dir %s [0x%x]\n", dir->path->buf, fflags);
      if ((fflags & (NOTE_DELETE|NOTE_RENAME|NOTE_REVOKE)) &&
          w_string_equal(dir->path, root->root_path)) {
        w_log(W_LOG_ERR,
            "root dir %s has been (re)moved [code 0x%x], canceling watch\n",
            root->root_path->buf, fflags);
        w_root_cancel(root);
        return 0;
      }
      w_pending_coll_add(coll, dir->path, false, now, false);
    } else {
      // NetBSD defines udata as intptr type, so the cast is necessary
      struct watchman_file *file = (void *)state->keventbuf[i].udata;

      w_string_t *path;

      path = w_string_path_cat(file->parent->path, file->name);
      w_pending_coll_add(coll, path, true, now, true);
      w_log(W_LOG_DBG, " KQ file %.*s [0x%x]\n", path->len, path->buf, fflags);
      w_string_delref(path);
    }
  }

  return n > 0;
}
Exemple #17
0
static void dispose_name(void *data)
{
  struct name_data *name = data;

  if (name->map) {
    w_ht_free(name->map);
  }
  if (name->name) {
    w_string_delref(name->name);
  }
  free(name);
}
Exemple #18
0
static bool parse_paths(w_query *res, json_t *query)
{
  json_t *paths;
  size_t i;

  paths = json_object_get(query, "path");
  if (!paths) {
    return true;
  }

  if (!json_is_array(paths)) {
    res->errmsg = strdup("'path' must be an array");
    return false;
  }

  res->npaths = json_array_size(paths);
  res->paths = calloc(res->npaths, sizeof(res->paths[0]));

  if (!res->paths) {
    res->errmsg = strdup("out of memory");
    return false;
  }

  for (i = 0; i < json_array_size(paths); i++) {
    json_t *ele = json_array_get(paths, i);
    const char *name = NULL;
    w_string_t *path;

    res->paths[i].depth = -1;

    if (json_is_string(ele)) {
      name = json_string_value(ele);
    } else if (json_unpack(ele, "{s:s, s:i}",
          "path", &name,
          "depth", &res->paths[i].depth
          ) != 0) {
      res->errmsg = strdup(
          "expected object with 'path' and 'depth' properties"
          );
      return false;
    }

    path = w_string_new(name);
    res->paths[i].name = w_string_canon_path(path);
    w_string_delref(path);
  }

  return true;
}
Exemple #19
0
static w_string_t *compute_parent_path(struct w_query_ctx *ctx,
                                       struct watchman_file *file) {
  if (ctx->last_parent == file->parent) {
    return ctx->last_parent_path;
  }

  if (ctx->last_parent_path) {
    w_string_delref(ctx->last_parent_path);
  }

  ctx->last_parent_path = w_dir_copy_full_path(file->parent);
  ctx->last_parent = file->parent;

  return ctx->last_parent_path;
}
Exemple #20
0
w_string& w_string::operator=(const w_string& other) {
  if (&other == this) {
    return *this;
  }

  reset();
  if (str_) {
    w_string_delref(str_);
  }
  str_ = other.str_;
  if (str_) {
    w_string_addref(str_);
  }

  return *this;
}
Exemple #21
0
/* trigger-del /root triggername
 * Delete a trigger from a root
 */
static void cmd_trigger_delete(struct watchman_client *client, json_t *args)
{
    w_root_t *root;
    json_t *resp;
    json_t *jname;
    w_string_t *tname;
    bool res;

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

    if (json_array_size(args) != 3) {
        send_error_response(client, "wrong number of arguments");
        w_root_delref(root);
        return;
    }
    jname = json_array_get(args, 2);
    if (!json_is_string(jname)) {
        send_error_response(client, "expected 2nd parameter to be trigger name");
        w_root_delref(root);
        return;
    }
    tname = json_to_w_string_incref(jname);

    w_root_lock(root, "trigger-del");
    res = w_ht_del(root->commands, w_ht_ptr_val(tname));
    w_root_unlock(root);

    if (res) {
        w_state_save();
    }

    w_string_delref(tname);

    resp = make_response();
    set_prop(resp, "deleted", json_boolean(res));
    json_incref(jname);
    set_prop(resp, "trigger", jname);
    send_and_dispose_response(client, resp);
    w_root_delref(root);
}
Exemple #22
0
w_string_t *w_query_ctx_get_wholename(
    struct w_query_ctx *ctx
)
{
  w_string_t *full_name;
  uint32_t name_start;

  if (ctx->wholename) {
    return ctx->wholename;
  }

  name_start = ctx->root->root_path->len + 1;

  full_name = w_string_path_cat(ctx->file->parent->path, ctx->file->name);
  // Record the name relative to the root
  ctx->wholename = w_string_slice(full_name, name_start,
      full_name->len - name_start);
  w_string_delref(full_name);

  return ctx->wholename;
}
Exemple #23
0
/* trigger-del /root triggername
 * Delete a trigger from a root
 */
void cmd_trigger_delete(struct watchman_client *client, json_t *args)
{
  w_root_t *root;
  json_t *resp;
  const char *name;
  w_string_t *tname;
  bool res;

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

  if (json_array_size(args) != 3) {
    send_error_response(client, "wrong number of arguments");
    w_root_delref(root);
    return;
  }
  name = json_string_value(json_array_get(args, 2));
  if (!name) {
    send_error_response(client, "expected 2nd parameter to be trigger name");
    w_root_delref(root);
    return;
  }
  tname = w_string_new(name);

  w_root_lock(root);
  res = w_ht_del(root->commands, (w_ht_val_t)tname);
  w_root_unlock(root);

  w_state_save();

  w_string_delref(tname);

  resp = make_response();
  set_prop(resp, "deleted", json_boolean(res));
  set_prop(resp, "trigger", json_string_nocheck(name));
  send_and_dispose_response(client, resp);
  w_root_delref(root);
}
Exemple #24
0
/* unsubscribe /root subname
 * Cancels a subscription */
static void cmd_unsubscribe(struct watchman_client *clientbase, json_t *args)
{
  w_root_t *root;
  const char *name;
  w_string_t *sname;
  bool deleted;
  json_t *resp;
  const json_t *jstr;
  struct watchman_user_client *client =
      (struct watchman_user_client *)clientbase;

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

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

  sname = json_to_w_string_incref(jstr);

  pthread_mutex_lock(&w_client_lock);
  deleted = w_ht_del(client->subscriptions, w_ht_ptr_val(sname));
  pthread_mutex_unlock(&w_client_lock);

  w_string_delref(sname);

  resp = make_response();
  set_bytestring_prop(resp, "unsubscribe", name);
  set_prop(resp, "deleted", json_boolean(deleted));

  send_and_dispose_response(&client->client, resp);
  w_root_delref(root);
}
Exemple #25
0
static struct watchman_command_handler_def *lookup(
    json_t *args, char **errmsg, int mode)
{
  struct watchman_command_handler_def *def;
  const char *cmd_name;
  w_string_t *cmd;

  if (!json_array_size(args)) {
    ignore_result(asprintf(errmsg,
        "invalid command (expected an array with some elements!)"));
    return false;
  }

  cmd_name = json_string_value(json_array_get(args, 0));
  if (!cmd_name) {
    ignore_result(asprintf(errmsg,
        "invalid command: expected element 0 to be the command name"));
    return false;
  }
  cmd = w_string_new(cmd_name);
  def = w_ht_val_ptr(w_ht_get(command_funcs, w_ht_ptr_val(cmd)));
  w_string_delref(cmd);

  if (def) {
    if (mode && ((def->flags & mode) == 0)) {
      ignore_result(asprintf(errmsg,
          "command %s not available in this mode", cmd_name));
      return NULL;
    }
    return def;
  }

  if (mode) {
    ignore_result(asprintf(errmsg, "unknown command %s", cmd_name));
  }

  return NULL;
}
Exemple #26
0
static bool eval_name(struct w_query_ctx *ctx,
    struct watchman_file *file,
    void *data)
{
  struct name_data *name = data;
  w_string_t *str;

  if (name->wholename) {
    str = w_query_ctx_get_wholename(ctx);
  } else {
    str = file->name;
  }

  if (name->map) {
    bool matched;
    w_ht_val_t val;

    if (name->caseless) {
      str = w_string_dup_lower(str);
      if (!str) {
        return false;
      }
    }

    matched = w_ht_lookup(name->map, w_ht_ptr_val(str), &val, false);

    if (name->caseless) {
      w_string_delref(str);
    }

    return matched;
  }

  if (name->caseless) {
    return w_string_equal_caseless(str, name->name);
  }
  return w_string_equal(str, name->name);
}
Exemple #27
0
void w_trigger_command_free(struct watchman_trigger_command *cmd)
{
  if (cmd->triggername) {
    w_string_delref(cmd->triggername);
  }

  if (cmd->command) {
    json_decref(cmd->command);
  }

  if (cmd->definition) {
    json_decref(cmd->definition);
  }

  if (cmd->query) {
    w_query_delref(cmd->query);
  }

  if (cmd->envht) {
    w_ht_free(cmd->envht);
  }

  free(cmd);
}
Exemple #28
0
void w_string_delref(w_string_t *str)
{
  if (!w_refcnt_del(&str->refcnt)) return;
  if (str->slice) w_string_delref(str->slice);
  free(str);
}
Exemple #29
0
static bool kqueue_root_consume_notify(watchman_global_watcher_t watcher,
    w_root_t *root, struct watchman_pending_collection *coll)
{
  struct kqueue_root_state *state = root->watch;
  int n;
  int i;
  struct timespec ts = { 0, 0 };
  struct timeval now;
  unused_parameter(watcher);

  errno = 0;
  n = kevent(state->kq_fd, NULL, 0,
      state->keventbuf, sizeof(state->keventbuf) / sizeof(state->keventbuf[0]),
      &ts);
  w_log(W_LOG_DBG, "consume_kqueue: %s n=%d err=%s\n",
      root->root_path->buf, n, strerror(errno));
  if (root->cancelled) {
    return 0;
  }

  gettimeofday(&now, NULL);
  for (i = 0; n > 0 && i < n; i++) {
    uint32_t fflags = state->keventbuf[i].fflags;
    bool is_dir = IS_DIR_BIT_SET(state->keventbuf[i].udata);
    w_string_t *path;
    char flags_label[128];
    int fd = state->keventbuf[i].ident;

    w_expand_flags(kflags, fflags, flags_label, sizeof(flags_label));
    pthread_mutex_lock(&state->lock);
    path = w_ht_val_ptr(w_ht_get(state->fd_to_name, fd));
    if (!path) {
      // Was likely a buffered notification for something that we decided
      // to stop watching
      w_log(W_LOG_DBG,
          " KQ notif for fd=%d; flags=0x%x %s no ref for it in fd_to_name\n",
          fd, fflags, flags_label);
      pthread_mutex_unlock(&state->lock);
      continue;
    }
    w_string_addref(path);

    w_log(W_LOG_DBG, " KQ fd=%d path %s [0x%x %s]\n",
        fd, path->buf, fflags, flags_label);
    if ((fflags & (NOTE_DELETE|NOTE_RENAME|NOTE_REVOKE))) {
      struct kevent k;

      if (w_string_equal(path, root->root_path)) {
        w_log(W_LOG_ERR,
            "root dir %s has been (re)moved [code 0x%x], canceling watch\n",
            root->root_path->buf, fflags);
        w_root_cancel(root);
        pthread_mutex_unlock(&state->lock);
        return 0;
      }

      // Remove our watch bits
      memset(&k, 0, sizeof(k));
      EV_SET(&k, fd, EVFILT_VNODE, EV_DELETE, 0, 0, NULL);
      kevent(state->kq_fd, &k, 1, NULL, 0, 0);
      w_ht_del(state->name_to_fd, w_ht_ptr_val(path));
      w_ht_del(state->fd_to_name, fd);
    }

    pthread_mutex_unlock(&state->lock);
    w_pending_coll_add(coll, path, !is_dir, now, !is_dir);
    w_string_delref(path);
  }

  return n > 0;
}
Exemple #30
0
static void *readchanges_thread(void *arg) {
  w_root_t *root = arg;
  struct winwatch_root_state *state = root->watch;
  DWORD size = WATCHMAN_BATCH_LIMIT * (sizeof(FILE_NOTIFY_INFORMATION) + 512);
  char *buf;
  DWORD err, filter;
  OVERLAPPED olap;
  BOOL initiate_read = true;
  HANDLE handles[2] = { state->olap, state->ping };
  DWORD bytes;

  w_set_thread_name("readchange %.*s",
      root->root_path->len, root->root_path->buf);

  // Block until winmatch_root_st is waiting for our initialization
  pthread_mutex_lock(&state->mtx);

  filter = FILE_NOTIFY_CHANGE_FILE_NAME|FILE_NOTIFY_CHANGE_DIR_NAME|
    FILE_NOTIFY_CHANGE_ATTRIBUTES|FILE_NOTIFY_CHANGE_SIZE|
    FILE_NOTIFY_CHANGE_LAST_WRITE;

  memset(&olap, 0, sizeof(olap));
  olap.hEvent = state->olap;

  buf = malloc(size);
  if (!buf) {
    w_log(W_LOG_ERR, "failed to allocate %u bytes for dirchanges buf\n", size);
    goto out;
  }

  if (!ReadDirectoryChangesW(state->dir_handle, buf, size,
        TRUE, filter, NULL, &olap, NULL)) {
    err = GetLastError();
    w_log(W_LOG_ERR,
        "ReadDirectoryChangesW: failed, cancel watch. %s\n",
        win32_strerror(err));
    w_root_lock(root);
    w_root_cancel(root);
    w_root_unlock(root);
    goto out;
  }
  // Signal that we are done with init.  We MUST do this AFTER our first
  // successful ReadDirectoryChangesW, otherwise there is a race condition
  // where we'll miss observing the cookie for a query that comes in
  // after we've crawled but before the watch is established.
  w_log(W_LOG_DBG, "ReadDirectoryChangesW signalling as init done");
  pthread_cond_signal(&state->cond);
  pthread_mutex_unlock(&state->mtx);
  initiate_read = false;

  // The state->mutex must not be held when we enter the loop
  while (!root->cancelled) {
    if (initiate_read) {
      if (!ReadDirectoryChangesW(state->dir_handle,
            buf, size, TRUE, filter, NULL, &olap, NULL)) {
        err = GetLastError();
        w_log(W_LOG_ERR,
            "ReadDirectoryChangesW: failed, cancel watch. %s\n",
            win32_strerror(err));
        w_root_lock(root);
        w_root_cancel(root);
        w_root_unlock(root);
        break;
      } else {
        initiate_read = false;
      }
    }

    w_log(W_LOG_DBG, "waiting for change notifications");
    DWORD status = WaitForMultipleObjects(2, handles, FALSE, INFINITE);

    if (status == WAIT_OBJECT_0) {
      bytes = 0;
      if (!GetOverlappedResult(state->dir_handle, &olap,
            &bytes, FALSE)) {
        err = GetLastError();
        w_log(W_LOG_ERR, "overlapped ReadDirectoryChangesW(%s): 0x%x %s\n",
            root->root_path->buf,
            err, win32_strerror(err));

        if (err == ERROR_INVALID_PARAMETER && size > NETWORK_BUF_SIZE) {
          // May be a network buffer related size issue; the docs say that
          // we can hit this when watching a UNC path. Let's downsize and
          // retry the read just one time
          w_log(W_LOG_ERR, "retrying watch for possible network location %s "
              "with smaller buffer\n", root->root_path->buf);
          size = NETWORK_BUF_SIZE;
          initiate_read = true;
          continue;
        }

        if (err == ERROR_NOTIFY_ENUM_DIR) {
          w_root_schedule_recrawl(root, "ERROR_NOTIFY_ENUM_DIR");
        } else {
          w_log(W_LOG_ERR, "Cancelling watch for %s\n",
              root->root_path->buf);
          w_root_lock(root);
          w_root_cancel(root);
          w_root_unlock(root);
          break;
        }
      } else {
        PFILE_NOTIFY_INFORMATION not = (PFILE_NOTIFY_INFORMATION)buf;
        struct winwatch_changed_item *head = NULL, *tail = NULL;
        while (true) {
          struct winwatch_changed_item *item;
          DWORD n_chars;
          w_string_t *name, *full;

          // FileNameLength is in BYTES, but FileName is WCHAR
          n_chars = not->FileNameLength / sizeof(not->FileName[0]);
          name = w_string_new_wchar(not->FileName, n_chars);

          full = w_string_path_cat(root->root_path, name);
          w_string_delref(name);

          if (w_is_ignored(root, full->buf, full->len)) {
            w_string_delref(full);
          } else {
            item = calloc(1, sizeof(*item));
            item->name = full;

            if (tail) {
              tail->next = item;
            } else {
              head = item;
            }
            tail = item;
          }

          // Advance to next item
          if (not->NextEntryOffset == 0) {
            break;
          }
          not = (PFILE_NOTIFY_INFORMATION)(not->NextEntryOffset + (char*)not);
        }

        if (tail) {
          pthread_mutex_lock(&state->mtx);
          if (state->tail) {
            state->tail->next = head;
          } else {
            state->head = head;
          }
          state->tail = tail;
          pthread_mutex_unlock(&state->mtx);
          pthread_cond_signal(&state->cond);
        }
        ResetEvent(state->olap);
        initiate_read = true;
      }
    } else if (status == WAIT_OBJECT_0 + 1) {
      w_log(W_LOG_ERR, "signalled\n");
      break;
    } else {
      w_log(W_LOG_ERR, "impossible wait status=%d\n",
          status);
      break;
    }
  }

  pthread_mutex_lock(&state->mtx);
out:
  // Signal to winwatch_root_start that we're done initializing in
  // the failure path.  We'll also do this after we've completed
  // the run loop in the success path; it's a spurious wakeup but
  // harmless and saves us from adding and setting a control flag
  // in each of the failure `goto` statements. winwatch_root_dtor
  // will `pthread_join` us before `state` is freed.
  pthread_cond_signal(&state->cond);
  pthread_mutex_unlock(&state->mtx);

  if (buf) {
    free(buf);
  }
  w_log(W_LOG_DBG, "done\n");
  w_root_delref(root);
  return NULL;
}