diff --git a/src/config.c b/src/config.c index 57e96a9..34cfeda 100644 --- a/src/config.c +++ b/src/config.c @@ -8,6 +8,7 @@ #include #include #include +#include #include "action.h" #include "config.h" @@ -15,6 +16,140 @@ #include "menu.h" #include "util.h" +struct nm_config_file_t { + char *path; + struct timespec mtime; + nm_config_file_t *next; +}; + + +// nm_config_files_filter skips special files, including: +// - dotfiles +// - vim: .*~, .*.s?? (unix only, usually swp or swo), *.swp, *.swo +// - gedit: .*~ +// - emacs: .*~, #*# +// - kate: .*.kate-swp +// - macOS: .DS_Store*, .Spotlight-V*, ._* +// - Windows: [Tt]humbs.db, desktop.ini +static int nm_config_files_filter(const struct dirent *de) { + const char *bn = de->d_name; + char *ex = strrchr(bn, '.'); + ex = (ex && ex != bn && bn[0] != '.') ? ex : NULL; + char lc = bn[strlen(bn)-1]; + if ((bn[0] == '.') || + (lc == '~') || + (bn[0] == '#' && lc == '#') || + (ex && (!strcmp(ex, ".swo") || !strcmp(ex, ".swp"))) || + (!strcmp(&bn[1], "humbs.db") && tolower(bn[0]) == 't') || + (!strcmp(bn, "desktop.ini"))) { + NM_LOG("config: skipping %s/%s because it's a special file", NM_CONFIG_DIR, de->d_name); + return 0; + } + return 1; +} + +nm_config_file_t *nm_config_files(char **err_out) { + #define NM_ERR_RET NULL + + nm_config_file_t *cfs = NULL, *cfc = NULL; + + struct dirent **nl; + int n = scandir(NM_CONFIG_DIR, &nl, nm_config_files_filter, alphasort); + NM_ASSERT(n != -1, "could not scan config dir: %s", strerror(errno)); + + for (int i = 0; i < n; i++) { + struct dirent *de = nl[i]; + + char *fn; + if (asprintf(&fn, "%s/%s", NM_CONFIG_DIR, de->d_name) == -1) + fn = NULL; + + struct stat statbuf; + if (!fn || stat(fn, &statbuf)) { + while (cfs) { + nm_config_file_t *tmp = cfs->next; + free(cfs); + cfs = tmp; + } + if (!fn) + NM_RETURN_ERR("could not build full path for config file"); + free(fn); + NM_RETURN_ERR("could not stat %s/%s", NM_CONFIG_DIR, de->d_name); + } + + // skip it if it isn't a file + if (de->d_type != DT_REG && !S_ISREG(statbuf.st_mode)) { + NM_LOG("config: skipping %s because not a regular file", fn); + free(fn); + continue; + } + + if (cfc) { + cfc->next = calloc(1, sizeof(nm_config_file_t)); + cfc = cfc->next; + } else { + cfs = calloc(1, sizeof(nm_config_file_t)); + cfc = cfs; + } + + cfc->path = fn; + cfc->mtime = statbuf.st_mtim; + + free(de); + } + + free(nl); + + NM_RETURN_OK(cfs); + #undef NM_ERR_RET +} + +bool nm_config_files_update(nm_config_file_t **files, char **err_out) { + #define NM_ERR_RET false + NM_ASSERT(files, "files pointer must not be null"); + + nm_config_file_t *nfiles = nm_config_files(err_out); + if (*err_out) + return NM_ERR_RET; + + if (!*files) { + *files = nfiles; + NM_RETURN_OK(true); + } + + bool ch = false; + nm_config_file_t *op = *files; + nm_config_file_t *np = nfiles; + + while (op && np) { + if (strcmp(op->path, np->path) || op->mtime.tv_sec != np->mtime.tv_sec || op->mtime.tv_nsec != np->mtime.tv_nsec) { + ch = true; + break; + } + op = op->next; + np = np->next; + } + + if (ch || op || np) { + nm_config_files_free(*files); + *files = nfiles; + NM_RETURN_OK(true); + } else { + nm_config_files_free(nfiles); + NM_RETURN_OK(false); + } + + #undef NM_ERR_RET +} + +void nm_config_files_free(nm_config_file_t *files) { + while (files) { + nm_config_file_t *tmp = files->next; + free(files); + files = tmp; + } +} + typedef enum { NM_CONFIG_TYPE_MENU_ITEM = 1, NM_CONFIG_TYPE_GENERATOR = 2, @@ -30,236 +165,161 @@ struct nm_config_t { nm_config_t *next; }; -static void nm_config_push_menu_item(nm_config_t **cfg, nm_menu_item_t *it) { - nm_config_t *tmp = calloc(1, sizeof(nm_config_t)); - tmp->type = NM_CONFIG_TYPE_MENU_ITEM; - tmp->value.menu_item = it; - tmp->next = *cfg; - *cfg = tmp; +// nm_config_parse__state_t contains the current state of the config parser. It +// should be initialized to zero. The nm_config_parse__append__* functions will +// deep-copy the item to append (to malloc'd memory). Each call will always +// leave the state consistent, even on error (i.e. it will always be safe to +// nm_config_free cfg_s). +typedef struct nm_config_parse__state_t { + nm_config_t *cfg_s; // config (first) + nm_config_t *cfg_c; // config (current) + + nm_menu_item_t *cfg_it_c; // menu item (current) + nm_menu_action_t *cfg_it_act_s; // menu action (first) + nm_menu_action_t *cfg_it_act_c; // menu action (current) + + nm_generator_t *cfg_gn_c; // generator (current) +} nm_config_parse__state_t; + +typedef enum nm_config_parse__append__ret_t { + NM_CONFIG_PARSE__APPEND__RET_OK = 0, + NM_CONFIG_PARSE__APPEND__RET_ALLOC_ERROR = 1, + NM_CONFIG_PARSE__APPEND__RET_ACTION_MUST_BE_AFTER_ITEM = 2, +} nm_config_parse__append__ret_t; + +static nm_config_parse__append__ret_t nm_config_parse__append_item(nm_config_parse__state_t *restrict state, nm_menu_item_t *const restrict it); // note: action pointer will be ignored (add it with append_action) +static nm_config_parse__append__ret_t nm_config_parse__append_action(nm_config_parse__state_t *restrict state, nm_menu_action_t *const restrict act); // note: next pointer will be ignored (add another one by calling this again) +static nm_config_parse__append__ret_t nm_config_parse__append_generator(nm_config_parse__state_t *restrict state, nm_generator_t *const restrict gn); + +static const char* nm_config_parse__strerror(nm_config_parse__append__ret_t ret) { + switch (ret) { + case NM_CONFIG_PARSE__APPEND__RET_OK: + return NULL; + case NM_CONFIG_PARSE__APPEND__RET_ALLOC_ERROR: + return "error allocating memory"; + case NM_CONFIG_PARSE__APPEND__RET_ACTION_MUST_BE_AFTER_ITEM: + return "unexpected chain, must be directly after a menu item or another chain"; + default: + return "unknown error"; + } } -static void nm_config_push_generator(nm_config_t **cfg, nm_generator_t *it) { - nm_config_t *tmp = calloc(1, sizeof(nm_config_t)); - tmp->type = NM_CONFIG_TYPE_GENERATOR; - tmp->value.generator = it; - tmp->next = *cfg; - *cfg = tmp; -} +// note: line must point to the part after the config line type, and will be +// modified. if the config line type doesn't match, everything will be +// left as-is and false will be returned without an error. if an error +// occurs, err_out will be set and true will be returned (since stuff +// may be modified). otherwise, true is returned without an error (the +// parsed output will have strings pointing directly into the line). -static void nm_config_push_action(nm_menu_action_t **cur, nm_menu_action_t *act) { - if (*cur) - (*cur)->next = act; - *cur = act; -} +static bool nm_config_parse__lineend_action(int field, char **line, bool p_on_success, bool p_on_failure, nm_menu_action_t *act_out, char **err_out); +static bool nm_config_parse__line_item(const char *type, char **line, nm_menu_item_t *it_out, nm_menu_action_t *action_out, char **err_out); +static bool nm_config_parse__line_chain(const char *type, char **line, nm_menu_action_t *act_out, char **err_out); +static bool nm_config_parse__line_generator(const char *type, char **line, nm_generator_t *gn_out, char **err_out); -nm_config_t *nm_config_parse(char **err_out) { +nm_config_t *nm_config_parse(nm_config_file_t *files, char **err_out) { #define NM_ERR_RET NULL - NM_LOG("config: reading config dir %s", NM_CONFIG_DIR); - // set up the linked list - nm_config_t *cfg = NULL; + char *err = NULL; - // open the config dir - DIR *cfgdir; - NM_ASSERT((cfgdir = opendir(NM_CONFIG_DIR)), "could not open config dir: %s", strerror(errno)); + FILE *cfgfile = NULL; + char *line = NULL; + size_t line_bufsz = 0; + int line_n; + ssize_t line_sz; - // loop over the dirents of the config dir - struct dirent *dirent; errno = 0; - while ((dirent = readdir(cfgdir))) { - char *fn; - NM_ASSERT(asprintf(&fn, "%s/%s", NM_CONFIG_DIR, dirent->d_name) != -1, "could not build full path for config file"); + nm_config_parse__append__ret_t ret; + nm_config_parse__state_t state = {0}; - // skip it if it isn't a file - bool reg = dirent->d_type == DT_REG; - if (dirent->d_type == DT_UNKNOWN) { - struct stat statbuf; - NM_ASSERT(!stat(fn, &statbuf), "could not stat %s", fn); - reg = S_ISREG(statbuf.st_mode); - } - if (!reg) { - NM_LOG("config: skipping %s because not a regular file", fn); - continue; - } + nm_menu_item_t tmp_it; + nm_menu_action_t tmp_act; + nm_generator_t tmp_gn; - // skip special files, including: - // - dotfiles - // - vim: .*~, .*.s?? (unix only, usually swp or swo), *.swp, *.swo - // - gedit: .*~ - // - emacs: .*~, #*# - // - kate: .*.kate-swp - // - macOS: .DS_Store*, .Spotlight-V*, ._* - // - Windows: [Tt]humbs.db, desktop.ini - char *bn = dirent->d_name; - char *ex = strrchr(bn, '.'); - ex = (ex && ex != bn && bn[0] != '.') ? ex : NULL; - char lc = bn[strlen(bn)-1]; - if ((bn[0] == '.') || - (lc == '~') || - (bn[0] == '#' && lc == '#') || - (ex && (!strcmp(ex, ".swo") || !strcmp(ex, ".swp"))) || - (!strcmp(&bn[1], "humbs.db") && tolower(bn[0]) == 't') || - (!strcmp(bn, "desktop.ini"))) { - NM_LOG("config: skipping %s because it's a special file", fn); - continue; - } + #define RETERR(fmt, ...) do { \ + if (cfgfile) \ + fclose(cfgfile); \ + if (err_out) \ + asprintf(err_out, fmt, ##__VA_ARGS__); \ + free(err); \ + free(line); \ + nm_config_free(state.cfg_c); \ + return NM_ERR_RET; \ + } while (0) - // open the config file - NM_LOG("config: reading config file %s", fn); - FILE *cfgfile; - NM_ASSERT((cfgfile = fopen(fn, "r")), "could not open file: %s", strerror(errno)); + for (nm_config_file_t *cf = files; cf; cf = cf->next) { + NM_LOG("config: reading config file %s", cf->path); - #define RETERR(fmt, ...) do { \ - fclose(cfgfile); \ - free(line); \ - closedir(cfgdir); \ - NM_RETURN_ERR(fmt, ##__VA_ARGS__); \ - } while (0) + line_n = 0; + cfgfile = fopen(cf->path, "r"); - // parse each line - char *line; - int line_n = 0; - ssize_t line_sz; - size_t line_bufsz = 0; + if (!cfgfile) + RETERR("could not open file: %s", strerror(errno)); - nm_menu_item_t *it = NULL; - nm_menu_action_t *cur_act = NULL; while ((line_sz = getline(&line, &line_bufsz, cfgfile)) != -1) { line_n++; - // empty line or comment char *cur = strtrim(line); if (!*cur || *cur == '#') + continue; // empty line or comment + + char *s_typ = strtrim(strsep(&cur, ":")); + + if (nm_config_parse__line_item(s_typ, &cur, &tmp_it, &tmp_act, &err)) { + if (err) + RETERR("file %s: line %d: parse menu_item: %s", cf->path, line_n, err); + if ((ret = nm_config_parse__append_item(&state, &tmp_it))) + RETERR("file %s: line %d: error appending item to config: %s", cf->path, line_n, nm_config_parse__strerror(ret)); + if ((ret = nm_config_parse__append_action(&state, &tmp_act))) + RETERR("file %s: line %d: error appending action to config: %s", cf->path, line_n, nm_config_parse__strerror(ret)); continue; + } - // field 1: type - char *c_typ = strtrim(strsep(&cur, ":")); - if (!strcmp(c_typ, "menu_item")) { - // type: menu_item - if (it) nm_config_push_menu_item(&cfg, it); - it = calloc(1, sizeof(nm_menu_item_t)); - cur_act = NULL; + if (nm_config_parse__line_chain(s_typ, &cur, &tmp_act, &err)) { + if (err) + RETERR("file %s: line %d: parse chain: %s", cf->path, line_n, err); + if ((ret = nm_config_parse__append_action(&state, &tmp_act))) + RETERR("file %s: line %d: error appending action to config: %s", cf->path, line_n, nm_config_parse__strerror(ret)); + continue; + } - // type: menu_item - field 2: location - char *c_loc = strtrim(strsep(&cur, ":")); - if (!c_loc) RETERR("file %s: line %d: field 2: expected location, got end of line", fn, line_n); - else if (!strcmp(c_loc, "main")) it->loc = NM_MENU_LOCATION_MAIN_MENU; - else if (!strcmp(c_loc, "reader")) it->loc = NM_MENU_LOCATION_READER_MENU; - else RETERR("file %s: line %d: field 2: unknown location '%s'", fn, line_n, c_loc); + if (nm_config_parse__line_generator(s_typ, &cur, &tmp_gn, &err)) { + if (err) + RETERR("file %s: line %d: parse generator: %s", cf->path, line_n, err); + if ((ret = nm_config_parse__append_generator(&state, &tmp_gn))) + RETERR("file %s: line %d: error appending generator to config: %s", cf->path, line_n, nm_config_parse__strerror(ret)); + continue; + } - // type: menu_item - field 3: label - char *c_lbl = strtrim(strsep(&cur, ":")); - if (!c_lbl) RETERR("file %s: line %d: field 3: expected label, got end of line", fn, line_n); - else it->lbl = strdup(c_lbl); - - // type: menu_item - field 4: action - nm_menu_action_t *action = calloc(1, sizeof(nm_menu_action_t)); - action->on_failure = true; - action->on_success = true; - char *c_act = strtrim(strsep(&cur, ":")); - if (!c_act) RETERR("file %s: line %d: field 4: expected action, got end of line", fn, line_n); - #define X(name) else if (!strcmp(c_act, #name)) action->act = NM_ACTION(name); - NM_ACTIONS - #undef X - else RETERR("file %s: line %d: field 4: unknown action '%s'", fn, line_n, c_act); - - // type: menu_item - field 5: argument - char *c_arg = strtrim(cur); - if (!c_arg) RETERR("file %s: line %d: field 5: expected argument, got end of line\n", fn, line_n); - else action->arg = strdup(c_arg); - nm_config_push_action(&cur_act, action); - it->action = cur_act; - } else if (!strncmp(c_typ, "chain", 5)) { - // type: chain - if (!it) RETERR("file %s: line %d: unexpected chain, no menu_item to link to", fn, line_n); - nm_menu_action_t *action = calloc(1, sizeof(nm_menu_action_t)); - - if (!strcmp(c_typ, "chain")) { - RETERR("file %s: line %d: field 1: the chain action has been renamed to chain_success", fn, line_n); - } else if (!strcmp(c_typ, "chain_success")) { - action->on_failure = false; - action->on_success = true; - } else if (!strcmp(c_typ, "chain_always")) { - action->on_failure = true; - action->on_success = true; - } else if (!strcmp(c_typ, "chain_failure")) { - action->on_failure = true; - action->on_success = false; - } else RETERR("file %s: line %d: field 1: unknown type '%s'", fn, line_n, c_typ); - - // type: chain - field 2: action - char *c_act = strtrim(strsep(&cur, ":")); - if (!c_act) RETERR("file %s: line %d: field 2: expected action, got end of line", fn, line_n); - #define X(name) else if (!strcmp(c_act, #name)) action->act = NM_ACTION(name); - NM_ACTIONS - #undef X - else RETERR("file %s: line %d: field 2: unknown action '%s'", fn, line_n, c_act); - - // type: chain - field 3: argument - char *c_arg = strtrim(cur); - if (!c_arg) RETERR("file %s: line %d: field 3: expected argument, got end of line\n", fn, line_n); - else action->arg = strdup(c_arg); - nm_config_push_action(&cur_act, action); - } else if (!strcmp(c_typ, "generator")) { - // type: generator - nm_generator_fn_t generate; - nm_menu_location_t loc; - - // type: generator - field 2: location - char *c_loc = strtrim(strsep(&cur, ":")); - if (!c_loc) RETERR("file %s: line %d: field 2: expected location, got end of line", fn, line_n); - else if (!strcmp(c_loc, "main")) loc = NM_MENU_LOCATION_MAIN_MENU; - else if (!strcmp(c_loc, "reader")) loc = NM_MENU_LOCATION_READER_MENU; - else RETERR("file %s: line %d: field 2: unknown location '%s'", fn, line_n, c_loc); - - // type: generator - field 3: generator - char *c_gen = strtrim(strsep(&cur, ":")); - if (!c_gen) RETERR("file %s: line %d: field 3: expected generator, got end of line", fn, line_n); - #define X(name) else if (!strcmp(c_gen, #name)) generate = NM_GENERATOR(name); - NM_GENERATORS - #undef X - else RETERR("file %s: line %d: field 3: unknown generator '%s'", fn, line_n, c_gen); - - // type: generator - field 4: argument (optional) - char *c_arg = strtrim(cur); - - nm_generator_t *gen = calloc(1, sizeof(nm_generator_t)); - gen->desc = strdup(c_gen); - gen->loc = loc; - gen->arg = strdup(c_arg ? c_arg : ""); - gen->generate = generate; - nm_config_push_generator(&cfg, gen); - } else RETERR("file %s: line %d: field 1: unknown type '%s'", fn, line_n, c_typ); + RETERR("file %s: line %d: field 1: unknown type '%s'", cf->path, line_n, s_typ); } - // Push the last menu item onto the config - if (it) nm_config_push_menu_item(&cfg, it); - it = NULL; - cur_act = NULL; - #undef RETERR + + // reset the current per-file state + state.cfg_it_c = NULL; + state.cfg_it_act_s = NULL; + state.cfg_it_act_c = NULL; + state.cfg_gn_c = NULL; fclose(cfgfile); - free(line); + cfgfile = NULL; } - NM_ASSERT(!errno, "could not read config dir: %s", strerror(errno)); - // close the config dir - closedir(cfgdir); + if (!state.cfg_c) { + if ((ret = nm_config_parse__append_item(&state, &(nm_menu_item_t){ + .loc = NM_MENU_LOCATION_MAIN_MENU, + .lbl = "NickelMenu", + .action = NULL, + }))) RETERR("error appending default item to empty config: %s", nm_config_parse__strerror(ret)); - // add a default entry if none were found - if (!cfg) { - nm_menu_item_t *it = calloc(1, sizeof(nm_menu_item_t)); - nm_menu_action_t *action = calloc(1, sizeof(nm_menu_action_t)); - it->loc = NM_MENU_LOCATION_MAIN_MENU; - it->lbl = strdup("NickelMenu"); - it->action = action; - action->arg = strdup("See .adds/nm/doc for instructions on how to customize this menu."); - action->act = NM_ACTION(dbg_toast); - action->on_failure = true; - action->on_success = true; - nm_config_push_menu_item(&cfg, it); + if ((ret = nm_config_parse__append_action(&state, &(nm_menu_action_t){ + .act = NM_ACTION(dbg_toast), + .on_failure = true, + .on_success = true, + .arg = "See .adds/nm/doc for instructions on how to customize this menu.", + .next = NULL, + }))) RETERR("error appending default action to empty config: %s", nm_config_parse__strerror(ret)); } size_t mm = 0, rm = 0; - for (nm_config_t *cur = cfg; cur; cur = cur->next) { + for (nm_config_t *cur = state.cfg_s; cur; cur = cur->next) { switch (cur->type) { case NM_CONFIG_TYPE_MENU_ITEM: NM_LOG("cfg(NM_CONFIG_TYPE_MENU_ITEM) : %d:%s", cur->value.menu_item->loc, cur->value.menu_item->lbl); @@ -275,14 +335,250 @@ nm_config_t *nm_config_parse(char **err_out) { break; } } - NM_ASSERT(mm <= NM_CONFIG_MAX_MENU_ITEMS_PER_MENU, "too many menu items in main menu (> %d)", NM_CONFIG_MAX_MENU_ITEMS_PER_MENU); - NM_ASSERT(rm <= NM_CONFIG_MAX_MENU_ITEMS_PER_MENU, "too many menu items in reader menu (> %d)", NM_CONFIG_MAX_MENU_ITEMS_PER_MENU); - // return the head of the list - NM_RETURN_OK(cfg); + if (mm > NM_CONFIG_MAX_MENU_ITEMS_PER_MENU) + RETERR("too many menu items in main menu (> %d)", NM_CONFIG_MAX_MENU_ITEMS_PER_MENU); + if (rm > NM_CONFIG_MAX_MENU_ITEMS_PER_MENU) + RETERR("too many menu items in reader menu (> %d)", NM_CONFIG_MAX_MENU_ITEMS_PER_MENU); + + NM_RETURN_OK(state.cfg_s); #undef NM_ERR_RET } +static bool nm_config_parse__line_item(const char *type, char **line, nm_menu_item_t *it_out, nm_menu_action_t *action_out, char **err_out) { + #define NM_ERR_RET true + + if (strcmp(type, "menu_item")) + NM_RETURN_OK(false); + + *it_out = (nm_menu_item_t){0}; + + char *s_loc = strtrim(strsep(line, ":")); + if (!s_loc) NM_RETURN_ERR("field 2: expected location, got end of line"); + else if (!strcmp(s_loc, "main")) it_out->loc = NM_MENU_LOCATION_MAIN_MENU; + else if (!strcmp(s_loc, "reader")) it_out->loc = NM_MENU_LOCATION_READER_MENU; + else NM_RETURN_ERR("field 2: unknown location '%s'", s_loc); + + char *p_lbl = strtrim(strsep(line, ":")); + if (!p_lbl) NM_RETURN_ERR("field 3: expected label, got end of line"); + it_out->lbl = p_lbl; + + nm_config_parse__lineend_action(4, line, true, true, action_out, err_out); + if (*err_out) + return NM_ERR_RET; + + NM_RETURN_OK(true); + #undef NM_ERR_RET +} + +static bool nm_config_parse__line_chain(const char *type, char **line, nm_menu_action_t *act_out, char **err_out) { + #define NM_ERR_RET true + + if (strncmp(type, "chain_", 5)) + NM_RETURN_OK(false); + + bool p_on_success, p_on_failure; + if (!strcmp(type, "chain_success")) { + p_on_success = true; + p_on_failure = false; + } else if (!strcmp(type, "chain_always")) { + p_on_success = true; + p_on_failure = true; + } else if (!strcmp(type, "chain_failure")) { + p_on_success = false; + p_on_failure = true; + } else NM_RETURN_OK(false); + + nm_config_parse__lineend_action(2, line, p_on_success, p_on_failure, act_out, err_out); + if (*err_out) + return NM_ERR_RET; + + NM_RETURN_OK(true); + #undef NM_ERR_RET +} + +static bool nm_config_parse__line_generator(const char *type, char **line, nm_generator_t *gn_out, char **err_out) { + #define NM_ERR_RET true + + if (strcmp(type, "generator")) + NM_RETURN_OK(false); + + *gn_out = (nm_generator_t){0}; + + char *s_loc = strtrim(strsep(line, ":")); + if (!s_loc) NM_RETURN_ERR("field 2: expected location, got end of line"); + else if (!strcmp(s_loc, "main")) gn_out->loc = NM_MENU_LOCATION_MAIN_MENU; + else if (!strcmp(s_loc, "reader")) gn_out->loc = NM_MENU_LOCATION_READER_MENU; + else NM_RETURN_ERR("field 2: unknown location '%s'", s_loc); + + char *s_generate = strtrim(strsep(line, ":")); + if (!s_generate) NM_RETURN_ERR("field 3: expected generator, got end of line"); + #define X(name) \ + else if (!strcmp(s_generate, #name)) gn_out->generate = NM_GENERATOR(name); + NM_GENERATORS + #undef X + else NM_RETURN_ERR("field 3: unknown generator '%s'", s_generate); + + char *p_arg = strtrim(*line); // note: optional + if (p_arg) gn_out->arg = p_arg; + + gn_out->desc = s_generate; + + NM_RETURN_OK(true); + #undef NM_ERR_RET +} + +static bool nm_config_parse__lineend_action(int field, char **line, bool p_on_success, bool p_on_failure, nm_menu_action_t *act_out, char **err_out) { + #define NM_ERR_RET true + + *act_out = (nm_menu_action_t){0}; + + char *s_act = strtrim(strsep(line, ":")); + if (!s_act) NM_RETURN_ERR("field %d: expected action, got end of line", field); + #define X(name) \ + else if (!strcmp(s_act, #name)) act_out->act = NM_ACTION(name); + NM_ACTIONS + #undef X + else NM_RETURN_ERR("field %d: unknown action '%s'", field, s_act); + + // type: menu_item - field 5: argument + char *p_arg = strtrim(*line); + if (!p_arg) NM_RETURN_ERR("field %d: expected argument, got end of line\n", field+1); + act_out->arg = p_arg; + + act_out->on_success = p_on_success; + act_out->on_failure = p_on_failure; + + NM_RETURN_OK(true); + #undef NM_ERR_RET +} + +static nm_config_parse__append__ret_t nm_config_parse__append_item(nm_config_parse__state_t *restrict state, nm_menu_item_t *const restrict it) { + nm_config_t *cfg_n = calloc(1, sizeof(nm_config_t)); + nm_menu_item_t *cfg_it_n = calloc(1, sizeof(nm_menu_item_t)); + + if (!cfg_n || !cfg_it_n) { + free(cfg_n); + free(cfg_it_n); + return NM_CONFIG_PARSE__APPEND__RET_ALLOC_ERROR; + } + + *cfg_n = (nm_config_t){ + .type = NM_CONFIG_TYPE_MENU_ITEM, + .generated = false, + .value = { .menu_item = cfg_it_n }, + .next = NULL, + }; + + *cfg_it_n = (nm_menu_item_t){ + .loc = it->loc, + .lbl = strdup(it->lbl ? it->lbl : ""), + .action = NULL, + }; + + if (!cfg_it_n->lbl) { + free(cfg_n); + free(cfg_it_n); + return NM_CONFIG_PARSE__APPEND__RET_ALLOC_ERROR; + } + + if (state->cfg_c) + state->cfg_c->next = cfg_n; + else + state->cfg_s = cfg_n; + + state->cfg_c = cfg_n; + state->cfg_it_c = cfg_it_n; + + state->cfg_it_act_s = NULL; + state->cfg_it_act_c = NULL; + state->cfg_gn_c = NULL; + + return NM_CONFIG_PARSE__APPEND__RET_OK; +} + +static nm_config_parse__append__ret_t nm_config_parse__append_action(nm_config_parse__state_t *restrict state, nm_menu_action_t *const restrict act) { + if (!state->cfg_c || !state->cfg_it_c || state->cfg_gn_c) + return NM_CONFIG_PARSE__APPEND__RET_ACTION_MUST_BE_AFTER_ITEM; + + nm_menu_action_t *cfg_it_act_n = calloc(1, sizeof(nm_menu_action_t)); + + if (!cfg_it_act_n) + return NM_CONFIG_PARSE__APPEND__RET_ALLOC_ERROR; + + *cfg_it_act_n = (nm_menu_action_t){ + .act = act->act, + .on_failure = act->on_failure, + .on_success = act->on_success, + .arg = strdup(act->arg ? act->arg : ""), + .next = NULL, + }; + + if (!cfg_it_act_n->arg) { + free(cfg_it_act_n); + return NM_CONFIG_PARSE__APPEND__RET_ALLOC_ERROR; + } + + if (!state->cfg_it_c->action) + state->cfg_it_c->action = cfg_it_act_n; + + if (state->cfg_it_act_c) + state->cfg_it_act_c->next = cfg_it_act_n; + else + state->cfg_it_act_s = cfg_it_act_n; + + state->cfg_it_act_c = cfg_it_act_n; + + return NM_CONFIG_PARSE__APPEND__RET_OK; +} + +static nm_config_parse__append__ret_t nm_config_parse__append_generator(nm_config_parse__state_t *restrict state, nm_generator_t *const restrict gn) { + nm_config_t *cfg_n = calloc(1, sizeof(nm_config_t)); + nm_generator_t *cfg_gn_n = calloc(1, sizeof(nm_generator_t)); + + if (!cfg_n || !cfg_gn_n) { + free(cfg_n); + free(cfg_gn_n); + return NM_CONFIG_PARSE__APPEND__RET_ALLOC_ERROR; + } + + *cfg_n = (nm_config_t){ + .type = NM_CONFIG_TYPE_GENERATOR, + .generated = false, + .value = { .generator = cfg_gn_n }, + .next = NULL, + }; + + *cfg_gn_n = (nm_generator_t){ + .desc = strdup(gn->desc ? gn->desc : ""), + .loc = gn->loc, + .arg = strdup(gn->arg ? gn->arg : ""), + .generate = gn->generate, + }; + + if (!cfg_gn_n->desc || !cfg_gn_n->arg) { + free(cfg_gn_n->desc); + free(cfg_gn_n->arg); + free(cfg_n); + free(cfg_gn_n); + return NM_CONFIG_PARSE__APPEND__RET_ALLOC_ERROR; + } + + if (state->cfg_c) + state->cfg_c->next = cfg_n; + else + state->cfg_s = cfg_n; + + state->cfg_c = cfg_n; + state->cfg_gn_c = cfg_gn_n; + + state->cfg_it_c = NULL; + state->cfg_it_act_s = NULL; + state->cfg_it_act_c = NULL; + + return NM_CONFIG_PARSE__APPEND__RET_OK; +} + void nm_config_generate(nm_config_t *cfg) { NM_LOG("config: removing any previously generated items"); for (nm_config_t *prev = NULL, *cur = cfg; cur; cur = cur->next) { @@ -309,7 +605,7 @@ void nm_config_generate(nm_config_t *cfg) { } NM_LOG("config: ... %zu items generated", sz); - for (size_t i = 0; i < sz; i++) { + for (ssize_t i = sz-1; i >= 0; i--) { nm_config_t *tmp = calloc(1, sizeof(nm_config_t)); tmp->type = NM_CONFIG_TYPE_MENU_ITEM; tmp->value.menu_item = items[i]; @@ -348,10 +644,10 @@ nm_menu_item_t **nm_config_get_menu(nm_config_t *cfg, size_t *n_out) { if (!it) return NULL; - nm_menu_item_t **tmp = &it[*n_out - 1]; + nm_menu_item_t **tmp = it; for (nm_config_t *cur = cfg; cur; cur = cur->next) if (cur->type == NM_CONFIG_TYPE_MENU_ITEM) - *(tmp--) = cur->value.menu_item; + *(tmp++) = cur->value.menu_item; return it; } diff --git a/src/config.h b/src/config.h index 864bd8f..b8f596d 100644 --- a/src/config.h +++ b/src/config.h @@ -17,10 +17,26 @@ extern "C" { typedef struct nm_config_t nm_config_t; -// nm_config_parse parses the configuration files in /mnt/onboard/.adds/nm. -// An error is returned if there are syntax errors, file access errors, or -// invalid action names for menu_item. -nm_config_t *nm_config_parse(char **err_out); +typedef struct nm_config_file_t nm_config_file_t; + +// nm_config_parse lists the configuration files in /mnt/onboard/.adds/nm. An +// error is returned if there are errors reading the dir. +nm_config_file_t *nm_config_files(char **err_out); + +// nm_config_files_update checks if the configuration files are up to date and +// updates them, returning true, if not. If *files is NULL, it is equivalent to +// doing `*files = nm_config_files(err_out)`. If an error occurs, the pointer is +// left untouched and false is returned along with the error. Warning: if the +// files have changed, the pointer passed to files will become invalid (it gets +// replaced). +bool nm_config_files_update(nm_config_file_t **files, char **err_out); + +// nm_config_files_free frees the list of configuration files. +void nm_config_files_free(nm_config_file_t *files); + +// nm_config_parse parses the configuration files. An error is returned if there +// are syntax errors, file access errors, or invalid action names for menu_item. +nm_config_t *nm_config_parse(nm_config_file_t *files, char **err_out); // nm_config_generate runs all generators synchronously and sequentially. Any // previously generated items are automatically removed. diff --git a/src/init.c b/src/init.c index 2cd71cd..31f1291 100644 --- a/src/init.c +++ b/src/init.c @@ -10,6 +10,7 @@ #include "action.h" #include "config.h" #include "failsafe.h" +#include "init.h" #include "menu.h" #include "util.h" @@ -30,6 +31,7 @@ __attribute__((constructor)) void nm_init() { NM_LOG("config dir: %s", NM_CONFIG_DIR); NM_LOG("init: creating failsafe"); + nm_failsafe_t *fs; if (!(fs = nm_failsafe_create(&err)) && err) { NM_LOG("error: could not create failsafe: %s, stopping", err); @@ -38,6 +40,7 @@ __attribute__((constructor)) void nm_init() { } NM_LOG("init: checking for uninstall flag"); + if (!access(NM_CONFIG_DIR "/uninstall", F_OK)) { NM_LOG("init: flag found, uninstalling"); nm_failsafe_uninstall(fs); @@ -47,6 +50,7 @@ __attribute__((constructor)) void nm_init() { #ifdef NM_UNINSTALL_CONFIGDIR NM_LOG("init: NM_UNINSTALL_CONFIGDIR: checking if config dir exists"); + if (access(NM_CONFIG_DIR, F_OK) && errno == ENOENT) { NM_LOG("init: config dir does not exist, uninstalling"); nm_failsafe_uninstall(fs); @@ -54,37 +58,27 @@ __attribute__((constructor)) void nm_init() { } #endif - NM_LOG("init: parsing config"); - size_t items_n; - nm_menu_item_t **items; - nm_config_t *cfg; - if (!(cfg = nm_config_parse(&err)) && err) { - NM_LOG("error: could not parse config: %s, creating error item in main menu instead", err); + NM_LOG("init: updating config"); - items_n = 1; - items = calloc(items_n, sizeof(nm_menu_item_t*)); - items[0] = calloc(1, sizeof(nm_menu_item_t)); - items[0]->loc = NM_MENU_LOCATION_MAIN_MENU; - items[0]->lbl = strdup("Config Error"); - items[0]->action = calloc(1, sizeof(nm_menu_action_t)); - items[0]->action->arg = strdup(err); - items[0]->action->act = NM_ACTION(dbg_msg); - items[0]->action->on_failure = true; - items[0]->action->on_success = true; + bool upd = nm_global_config_update(&err); + if (err) { + NM_LOG("init: error parsing config, will show a menu item with the error: %s", err); + } - free(err); - } else { - NM_LOG("init: generating items"); - nm_config_generate(cfg); - - NM_LOG("init: getting menu"); - if (!(items = nm_config_get_menu(cfg, &items_n))) { - NM_LOG("error: could not allocate memory, stopping"); - goto stop_fs; - } + size_t ntmp = SIZE_MAX; + if (!upd) { + NM_LOG("init: no config file changes detected for initial config update (it should always return an error or update), stopping (this is a bug; err should have been returned instead)"); + return; + } else if (!nm_global_config_items(&ntmp)) { + NM_LOG("init: warning: no menu items returned by nm_global_config_items, ignoring for now (this is a bug; it should always have a menu item whether the default, an error, or the actual config)"); + } else if (ntmp == SIZE_MAX) { + NM_LOG("init: warning: no size returned by nm_global_config_items, ignoring for now (this is a bug)"); + } else if (!ntmp) { + NM_LOG("init: warning: size returned by nm_global_config_items is 0, ignoring for now (this is a bug; it should always have a menu item whether the default, an error, or the actual config)"); } NM_LOG("init: opening libnickel"); + void *libnickel = dlopen("libnickel.so.1.0.0", RTLD_LAZY|RTLD_NODELETE); if (!libnickel) { NM_LOG("error: could not dlopen libnickel, stopping"); @@ -92,7 +86,8 @@ __attribute__((constructor)) void nm_init() { } NM_LOG("init: hooking libnickel"); - if (nm_menu_hook(libnickel, items, items_n, &err) && err) { + + if (nm_menu_hook(libnickel, &err) && err) { NM_LOG("error: could not hook libnickel: %s, stopping", err); free(err); goto stop_fs; @@ -106,3 +101,95 @@ stop: NM_LOG("init: done"); return; } + +// note: not thread safe +static nm_config_file_t *nm_global_menu_config_files = NULL; // updated in-place by nm_global_config_update +static nm_config_t *nm_global_menu_config = NULL; // updated by nm_global_config_update, replaced by nm_global_config_replace, NULL on error +static nm_menu_item_t **nm_global_menu_config_items = NULL; // updated by nm_global_config_replace to an error message or the items from nm_global_menu_config +static size_t nm_global_menu_config_n = 0; // ^ + +nm_menu_item_t **nm_global_config_items(size_t *n_out) { + if (n_out) + *n_out = nm_global_menu_config_n; + return nm_global_menu_config_items; +} + +static void nm_global_config_replace(nm_config_t *cfg, const char *err) { + if (nm_global_menu_config_n) + nm_global_menu_config_n = 0; + + if (nm_global_menu_config_items) { + free(nm_global_menu_config_items); + nm_global_menu_config_items = NULL; + } + + if (nm_global_menu_config) { + nm_config_free(nm_global_menu_config); + nm_global_menu_config = NULL; + } + + if (err && cfg) + nm_config_free(cfg); + + // this isn't strictly necessary, but we should always try to reparse it + // every time just in case the error was temporary + if (err && nm_global_menu_config_files) { + nm_config_files_free(nm_global_menu_config_files); + nm_global_menu_config_files = NULL; + } + + if (err) { + nm_global_menu_config_n = 1; + nm_global_menu_config_items = calloc(nm_global_menu_config_n, sizeof(nm_menu_item_t*)); + nm_global_menu_config_items[0] = calloc(1, sizeof(nm_menu_item_t)); + nm_global_menu_config_items[0]->loc = NM_MENU_LOCATION_MAIN_MENU; + nm_global_menu_config_items[0]->lbl = strdup("Config Error"); + nm_global_menu_config_items[0]->action = calloc(1, sizeof(nm_menu_action_t)); + nm_global_menu_config_items[0]->action->arg = strdup(err); + nm_global_menu_config_items[0]->action->act = NM_ACTION(dbg_msg); + nm_global_menu_config_items[0]->action->on_failure = true; + nm_global_menu_config_items[0]->action->on_success = true; + return; + } + + nm_global_menu_config_items = nm_config_get_menu(cfg, &nm_global_menu_config_n); + if (!nm_global_menu_config_items) + NM_LOG("could not allocate memory"); +} + +bool nm_global_config_update(char **err_out) { + #define NM_ERR_RET true + char *err; + + NM_LOG("global: scanning for config files"); + bool updated = nm_config_files_update(&nm_global_menu_config_files, &err); + if (err) { + NM_LOG("... error: %s", err); + NM_LOG("global: freeing old config and replacing with error item"); + nm_global_config_replace(NULL, err); + NM_RETURN_ERR("scan for config files: %s", err); + } + + NM_LOG("global:%s changes detected", updated ? "" : " no"); + if (!updated) + NM_RETURN_OK(false); + + NM_LOG("global: parsing new config"); + nm_config_t *cfg = nm_config_parse(nm_global_menu_config_files, &err); + if (err) { + NM_LOG("... error: %s", err); + NM_LOG("global: freeing old config and replacing with error item"); + nm_global_config_replace(NULL, err); + NM_RETURN_ERR("parse config files: %s", err); + } + + NM_LOG("global: running generators"); + nm_config_generate(cfg); + + NM_LOG("global: freeing old config and replacing with new one"); + nm_global_config_replace(cfg, NULL); + + NM_LOG("global: done swapping config"); + NM_RETURN_OK(true); + #undef NM_ERR_RET +} diff --git a/src/init.h b/src/init.h new file mode 100644 index 0000000..15916da --- /dev/null +++ b/src/init.h @@ -0,0 +1,28 @@ +#ifndef NM_INIT_H +#define NM_INIT_H +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include "menu.h" + +// nm_global_config_update updates and regenerates the config if needed, and +// returns true if it needed updating. If an error occurs, true is also returned +// since the menu items will be updated to a single one showing the error (see +// nm_global_config_items). +bool nm_global_config_update(char **err_out); + +// nm_global_config_items returns an array of pointers with the current menu +// items (the pointer and the items it points to will remain valid until the +// next time nm_global_config_update is called). The number of items is stored +// in the variable pointed to by n_out. If an error ocurred during the last time +// nm_global_config_update was called, it is returned as a "Config Error" menu +// item. If nm_global_config_update has never been called successfully before, +// NULL is returned and n_out is set to 0. +nm_menu_item_t **nm_global_config_items(size_t *n_out); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/menu.cc b/src/menu.cc index 914633f..253c040 100644 --- a/src/menu.cc +++ b/src/menu.cc @@ -7,8 +7,8 @@ #include #include -#include "action.h" #include "dlhook.h" +#include "init.h" #include "menu.h" #include "util.h" @@ -60,10 +60,7 @@ void (*MainWindowController_toast)(MainWindowController*, QString const&, QStrin static void (*LightMenuSeparator_LightMenuSeparator)(void*, QWidget*); static void (*BoldMenuSeparator_BoldMenuSeparator)(void*, QWidget*); -static nm_menu_item_t **_items; -static size_t _items_n; - -extern "C" int nm_menu_hook(void *libnickel, nm_menu_item_t **items, size_t items_n, char **err_out) { +extern "C" int nm_menu_hook(void *libnickel, char **err_out) { #define NM_ERR_RET 1 //libnickel 4.6 * _ZN28AbstractNickelMenuController18createMenuTextItemEP5QMenuRK7QStringbbS4_ reinterpret_cast(AbstractNickelMenuController_createMenuTextItem) = dlsym(libnickel, "_ZN28AbstractNickelMenuController18createMenuTextItemEP5QMenuRK7QStringbbS4_"); @@ -98,9 +95,6 @@ extern "C" int nm_menu_hook(void *libnickel, nm_menu_item_t **items, size_t item reinterpret_cast(AbstractNickelMenuController_createMenuTextItem_orig) = nm_dlhook(libnickel, "_ZN28AbstractNickelMenuController18createMenuTextItemEP5QMenuRK7QStringbbS4_", nmh, &err); NM_ASSERT(AbstractNickelMenuController_createMenuTextItem_orig, "failed to hook _ZN28AbstractNickelMenuController18createMenuTextItemEP5QMenuRK7QStringbbS4_: %s", err); - _items = items; - _items_n = items_n; - NM_RETURN_OK(0); #undef NM_ERR_RET } @@ -108,7 +102,8 @@ extern "C" int nm_menu_hook(void *libnickel, nm_menu_item_t **items, size_t item // AbstractNickelMenuController_createAction_before wraps // AbstractNickelMenuController::createAction to use the correct separator for // the menu location and to match the behaviour of QMenu::insertAction instead -// of QMenu::addAction. +// of QMenu::addAction. It also adds the property nm_action=true to the action +// and separator. QAction *AbstractNickelMenuController_createAction_before(QAction *before, nm_menu_location_t loc, bool last_in_group, void *_this, QMenu *menu, QWidget *widget, bool close, bool enabled, bool separator); // nm_menu_item_do runs a nm_menu_item_t and must be called from the thread of a @@ -143,6 +138,21 @@ extern "C" MenuTextItem* _nm_menu_hook(void* _this, QMenu* menu, QString const& void _nm_menu_inject(void *nmc, QMenu *menu, nm_menu_location_t loc, int at) { NM_LOG("inject %d @ %d", loc, at); + NM_LOG("checking for config updates"); + bool updated = nm_global_config_update(NULL); // if there was an error it will be returned as a menu item anyways (and updated will be true) + NM_LOG("updated = %d", updated); + + NM_LOG("checking for existing items added by nm"); + + for (auto action : menu->actions()) { + if (action->property("nm_action") == true) { + if (!updated) + return; // already added items, menu is up to date + menu->removeAction(action); + delete action; + } + } + NM_LOG("getting insertion point"); auto actions = menu->actions(); @@ -153,34 +163,31 @@ void _nm_menu_inject(void *nmc, QMenu *menu, nm_menu_location_t loc, int at) { if (before == nullptr) NM_LOG("it seems the original item to add new ones before was never actually added to the menu (number of items when the action was created is %d, current is %d), appending to end instead", at, actions.count()); - NM_LOG("checking for old items"); - - for (auto action : actions) { - if (action->property("nm_action") == true) { - return; // already added - /*menu->removeAction(action); - delete action;*/ - } - } - NM_LOG("injecting new items"); + size_t items_n; + nm_menu_item_t **items = nm_global_config_items(&items_n); + + if (!items) { + NM_LOG("items is NULL (either the config hasn't been parsed yet or there was a memory allocation error), not adding"); + return; + } + // if it segfaults in createMenuTextItem, it's likely because // AbstractNickelMenuController is invalid, which shouldn't happen while the // menu which we added the signal from still can be shown... (but // theoretically, it's possible) - for (size_t i = 0; i < _items_n; i++) { - nm_menu_item_t *it = _items[i]; + for (size_t i = 0; i < items_n; i++) { + nm_menu_item_t *it = items[i]; if (it->loc != loc) continue; NM_LOG("adding items '%s'...", it->lbl); MenuTextItem* item = AbstractNickelMenuController_createMenuTextItem_orig(nmc, menu, QString::fromUtf8(it->lbl), false, false, ""); - QAction* action = AbstractNickelMenuController_createAction_before(before, loc, i == _items_n-1, nmc, menu, item, true, true, true); + QAction* action = AbstractNickelMenuController_createAction_before(before, loc, i == items_n-1, nmc, menu, item, true, true, true); - action->setProperty("nm_action", true); QObject::connect(action, &QAction::triggered, [it](bool){ NM_LOG("item '%s' pressed...", it->lbl); nm_menu_item_do(it); @@ -262,6 +269,8 @@ QAction *AbstractNickelMenuController_createAction_before(QAction *before, nm_me int n = menu->actions().count(); QAction* action = AbstractNickelMenuController_createAction(_this, menu, widget, /*close*/false, enabled, /*separator*/false); + action->setProperty("nm_action", true); + if (!menu->actions().contains(action)) { NM_LOG("could not find added action at end of menu (note: old count is %d, new is %d), not moving it to the right spot or adding separator", n, menu->actions().count()); return action; @@ -279,16 +288,18 @@ QAction *AbstractNickelMenuController_createAction_before(QAction *before, nm_me if (separator) { // if it's the main menu, we generally want to use a custom separator + QAction *sep; if (loc == NM_MENU_LOCATION_MAIN_MENU && LightMenuSeparator_LightMenuSeparator && BoldMenuSeparator_BoldMenuSeparator) { - QAction *lsp = reinterpret_cast(calloc(1, 32)); // it's actually 8 as of 14622, but better to be safe + sep = reinterpret_cast(calloc(1, 32)); // it's actually 8 as of 14622, but better to be safe (last_in_group ? BoldMenuSeparator_BoldMenuSeparator : LightMenuSeparator_LightMenuSeparator - )(lsp, reinterpret_cast(_this)); - menu->insertAction(before, lsp); + )(sep, reinterpret_cast(_this)); + menu->insertAction(before, sep); } else { - menu->insertSeparator(before); + sep = menu->insertSeparator(before); } + sep->setProperty("nm_action", true); } return action; diff --git a/src/menu.h b/src/menu.h index 68c30ce..947b4fc 100644 --- a/src/menu.h +++ b/src/menu.h @@ -28,12 +28,9 @@ typedef struct { nm_menu_action_t *action; } nm_menu_item_t; -// nm_menu_hook hooks a dlopen'd libnickel handle to add the specified menus, -// and returns 0 on success, or 1 with err_out (if provided) set to the malloc'd -// error message on error. The provided configuration and all pointers it -// references must remain valid for the lifetime of the program (i.e. not stack -// allocated). It MUST NOT be called more than once. -int nm_menu_hook(void *libnickel, nm_menu_item_t **items, size_t items_n, char **err_out); +// nm_menu_hook hooks a dlopen'd libnickel handle. It MUST NOT be called more +// than once. +int nm_menu_hook(void *libnickel, char **err_out); #ifdef __cplusplus }