├── .github ├── CODEOWNERS └── workflows │ ├── test.yaml │ ├── build.yaml │ └── release.yaml ├── .gitmodules ├── .gitignore ├── test └── syms │ ├── go.mod │ ├── go.sum │ └── main.go ├── LICENSE ├── src ├── nickelmenu.h ├── util.c ├── action.c ├── action.h ├── util.h ├── generator.c ├── generator.h ├── kfmon.h ├── action_c.c ├── config.h ├── generator_c.c ├── kfmon_helpers.h ├── kfmon.c ├── config.c └── nickelmenu.cc ├── Makefile ├── README.md └── res └── doc /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @pgaskin 2 | src/kfmon* @NiLuJe -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "NickelHook"] 2 | path = NickelHook 3 | url = https://github.com/pgaskin/NickelHook.git 4 | branch = master 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # make gitignore 2 | .kdev4/ 3 | *.kdev4 4 | .kateconfig 5 | .vscode/ 6 | .idea/ 7 | .clangd/ 8 | .cache/ 9 | compile_commands.json 10 | /KoboRoot.tgz 11 | /src/libnm.so 12 | /src/action.o 13 | /src/action_c.o 14 | /src/config.o 15 | /src/generator.o 16 | /src/generator_c.o 17 | /src/kfmon.o 18 | /src/util.o 19 | /src/action_cc.o 20 | /src/nickelmenu.o 21 | test.syms 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | name: NickelMenu / Symbols 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v6 15 | with: 16 | submodules: true 17 | - name: Setup Go 18 | uses: actions/setup-go@v6 19 | with: 20 | go-version: '1.25' 21 | - name: Build 22 | run: cd test/syms && go build -o ../../test.syms . 23 | - name: Run 24 | run: cd src && ../test.syms 25 | -------------------------------------------------------------------------------- /test/syms/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pgaskin/NickelMenu/test/syms 2 | 3 | go 1.25.4 4 | 5 | require ( 6 | github.com/pgaskin/kobopatch v0.16.0 7 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 8 | ) 9 | 10 | require ( 11 | github.com/ianlancetaylor/demangle v0.0.0-20250628045327-2d64ad6b7ec5 // indirect 12 | github.com/pgaskin/go-libz v0.0.2 // indirect 13 | github.com/riking/cssparse v0.0.0-20180325025645-c37ded0aac89 // indirect 14 | github.com/tetratelabs/wazero v1.9.0 // indirect 15 | golang.org/x/text v0.3.2 // indirect 16 | rsc.io/arm v0.0.0-20150420010332-9c32f2193064 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | name: NickelMenu 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v6 15 | with: 16 | submodules: true 17 | - name: Build 18 | uses: docker://ghcr.io/pgaskin/nickeltc:1.0 19 | with: 20 | entrypoint: make 21 | args: all koboroot 22 | - name: Upload 23 | uses: actions/upload-artifact@v5 24 | with: 25 | name: NickelMenu 26 | path: KoboRoot.tgz 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2025 Patrick Gaskin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/nickelmenu.h: -------------------------------------------------------------------------------- 1 | #ifndef NM_H 2 | #define NM_H 3 | #ifdef __cplusplus 4 | extern "C" { 5 | #endif 6 | 7 | #include 8 | #include "action.h" 9 | 10 | #define NM_MENU_LOCATION(name) NM_MENU_LOCATION_##name 11 | 12 | #define NM_MENU_LOCATIONS \ 13 | X(main) \ 14 | X(reader) \ 15 | X(browser) \ 16 | X(library) \ 17 | X(selection) \ 18 | X(selection_search) 19 | 20 | typedef enum { 21 | NM_MENU_LOCATION_NONE = 0, // to allow it to be checked with if 22 | #define X(name) \ 23 | NM_MENU_LOCATION(name), 24 | NM_MENU_LOCATIONS 25 | #undef X 26 | } nm_menu_location_t; 27 | 28 | typedef struct nm_menu_action_t { 29 | char *arg; 30 | bool on_success; 31 | bool on_failure; 32 | nm_action_fn_t act; // can block, must return zero on success, nonzero with nm_err set on error 33 | struct nm_menu_action_t *next; 34 | } nm_menu_action_t; 35 | 36 | typedef struct { 37 | nm_menu_location_t loc; 38 | char *lbl; 39 | nm_menu_action_t *action; 40 | } nm_menu_item_t; 41 | 42 | #ifdef __cplusplus 43 | } 44 | #endif 45 | #endif 46 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include NickelHook/NickelHook.mk 2 | 3 | override PKGCONF += Qt5Widgets 4 | override LIBRARY := src/libnm.so 5 | override SOURCES += src/action.c src/action_c.c src/action_cc.cc src/config.c src/generator.c src/generator_c.c src/kfmon.c src/nickelmenu.cc src/util.c 6 | override CFLAGS += -Wall -Wextra -Werror -fvisibility=hidden 7 | override CXXFLAGS += -Wall -Wextra -Werror -Wno-missing-field-initializers -isystemlib -fvisibility=hidden -fvisibility-inlines-hidden 8 | override KOBOROOT += res/doc:$(NM_CONFIG_DIR)/doc 9 | 10 | override SKIPCONFIGURE += strip 11 | strip: 12 | $(STRIP) --strip-unneeded src/libnm.so 13 | .PHONY: strip 14 | 15 | ifeq ($(NM_UNINSTALL_CONFIGDIR),1) 16 | override CPPFLAGS += -DNM_UNINSTALL_CONFIGDIR 17 | endif 18 | 19 | ifeq ($(NM_CONFIG_DIR),) 20 | override NM_CONFIG_DIR := /mnt/onboard/.adds/nm 21 | endif 22 | 23 | ifneq ($(NM_CONFIG_DIR),/mnt/onboard/.adds/nm) 24 | $(info -- Warning: NM_CONFIG_DIR is set to a non-default value; this will cause issues with other mods using it!) 25 | endif 26 | 27 | override CPPFLAGS += -DNM_CONFIG_DIR='"$(NM_CONFIG_DIR)"' -DNM_CONFIG_DIR_DISP='"$(patsubst /mnt/onboard/%,KOBOeReader/%,$(NM_CONFIG_DIR))"' 28 | 29 | include NickelHook/NickelHook.mk 30 | -------------------------------------------------------------------------------- /src/util.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | static __thread bool nm_err_state = false; 7 | static __thread char nm_err_buf[2048] = {0}; 8 | static __thread char nm_err_buf_tmp[sizeof(nm_err_buf)] = {0}; // in case the format string overlaps 9 | 10 | const char *nm_err() { 11 | if (nm_err_state) { 12 | nm_err_state = false; 13 | return nm_err_buf; 14 | } 15 | return NULL; 16 | } 17 | 18 | const char *nm_err_peek() { 19 | if (nm_err_state) 20 | return nm_err_buf; 21 | return NULL; 22 | } 23 | 24 | bool nm_err_set(const char *fmt, ...) { 25 | va_list a; 26 | if ((nm_err_state = !!fmt)) { 27 | va_start(a, fmt); 28 | int r = vsnprintf(nm_err_buf_tmp, sizeof(nm_err_buf_tmp), fmt, a); 29 | if (r < 0) 30 | r = snprintf(nm_err_buf_tmp, sizeof(nm_err_buf_tmp), "error applying format to error string '%s'", fmt); 31 | if (r >= (int)(sizeof(nm_err_buf_tmp))) { 32 | nm_err_buf_tmp[sizeof(nm_err_buf_tmp) - 2] = '.'; 33 | nm_err_buf_tmp[sizeof(nm_err_buf_tmp) - 3] = '.'; 34 | nm_err_buf_tmp[sizeof(nm_err_buf_tmp) - 4] = '.'; 35 | } 36 | memcpy(nm_err_buf, nm_err_buf_tmp, sizeof(nm_err_buf)); 37 | va_end(a); 38 | } 39 | return nm_err_state; 40 | } 41 | -------------------------------------------------------------------------------- /src/action.c: -------------------------------------------------------------------------------- 1 | #define _GNU_SOURCE // vasprintf 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "action.h" 8 | 9 | nm_action_result_t *nm_action_result_silent() { 10 | nm_action_result_t *res = calloc(1, sizeof(nm_action_result_t)); 11 | res->type = NM_ACTION_RESULT_TYPE_SILENT; 12 | return res; 13 | } 14 | 15 | #define _nm_action_result_fmt(_fn, _typ) \ 16 | nm_action_result_t *nm_action_result_##_fn(const char *fmt, ...) { \ 17 | nm_action_result_t *res = calloc(1, sizeof(nm_action_result_t)); \ 18 | res->type = _typ; \ 19 | va_list v; \ 20 | va_start(v, fmt); \ 21 | if (vasprintf(&res->msg, fmt, v) == -1) \ 22 | res->msg = strdup("error"); \ 23 | va_end(v); \ 24 | return res; \ 25 | } 26 | 27 | _nm_action_result_fmt(msg, NM_ACTION_RESULT_TYPE_MSG); 28 | _nm_action_result_fmt(toast, NM_ACTION_RESULT_TYPE_TOAST); 29 | 30 | void nm_action_result_free(nm_action_result_t *res) { 31 | if (!res) 32 | return; 33 | if (res->msg) 34 | free(res->msg); 35 | free(res); 36 | } 37 | -------------------------------------------------------------------------------- /test/syms/go.sum: -------------------------------------------------------------------------------- 1 | github.com/ianlancetaylor/demangle v0.0.0-20250628045327-2d64ad6b7ec5 h1:QCtizt3VTaANvnsd8TtD/eonx7JLIVdEKW1//ZNPZ9A= 2 | github.com/ianlancetaylor/demangle v0.0.0-20250628045327-2d64ad6b7ec5/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= 3 | github.com/pgaskin/go-libz v0.0.2 h1:uTzyOb6qXvzTdofzq9RNzhpHzHl3fT88yM6wxzjrMFY= 4 | github.com/pgaskin/go-libz v0.0.2/go.mod h1:zKOJy/NMDudfyiyGiA7r0jAKjEyu+/7MSBHAnarLvxY= 5 | github.com/pgaskin/kobopatch v0.16.0 h1:4WczmEVXkIRjToiMqLF94wbIJTn5r5kAv41pJayPQaQ= 6 | github.com/pgaskin/kobopatch v0.16.0/go.mod h1:3xPtkRd97WkLtF2T3jsqTAkt1JU83xSAZ4QgEZzPanw= 7 | github.com/riking/cssparse v0.0.0-20180325025645-c37ded0aac89 h1:hMsoSMebpfpaDW7+B7gsxNnMBNChjekeqmK8wkzAlc0= 8 | github.com/riking/cssparse v0.0.0-20180325025645-c37ded0aac89/go.mod h1:yc5MYwuNUGggTQ8++IDAbOYq/9PXxsg73+EHYgoG/4w= 9 | github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= 10 | github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= 11 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= 12 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= 13 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 14 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 15 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 16 | rsc.io/arm v0.0.0-20150420010332-9c32f2193064 h1:bBbas3KhLwE6f59Z9lUipY23xUX9qrvyLBdQzzV2Tko= 17 | rsc.io/arm v0.0.0-20150420010332-9c32f2193064/go.mod h1:MVYPdlFruujBlzEY3x2Q3XBk7XLdYRNZ7zDbrzYFO7w= 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

NickelMenu

2 | 3 | The easiest way to launch custom scripts, change hidden settings, and run actions on Kobo eReaders. 4 | 5 | See the [website](https://pgaskin.net/NickelMenu) and [thread on MobileRead](https://mobileread.com/forums/showthread.php?t=329525) for screenshots and more details. 6 | 7 | Firmware 5.x is not supported yet. 8 | 9 | ## Installation 10 | You can download pre-built packages of the latest stable release from the [releases](https://github.com/pgaskin/NickelMenu/releases) page, or you can find bleeding-edge builds of each commit from [here](https://github.com/pgaskin/NickelMenu/actions). 11 | 12 | After you download the package, copy `KoboRoot.tgz` into the `.kobo` folder of your eReader, then eject it. 13 | 14 | After it installs, you will find a new menu item named `NickelMenu` with further instructions which you can also read [here](./res/doc). 15 | 16 | To uninstall NickelMenu, just create a new file named `uninstall` in `.adds/nm/`, or trigger the failsafe mechanism by immediately powering off the Kobo after it starts booting. 17 | 18 | Most errors, if any, will be displayed as a menu item in the main menu. If no new menu entries appear here after a reboot, try reinstalling NickelMenu. If that still doesn't work, connect over telnet or SSH and check the output of `logread`. 19 | 20 | ## Compiling 21 | 22 | NickelMenu is designed to be compiled with [NickelTC](https://github.com/pgaskin/NickelTC). To compile it with Docker/Podman, use `docker run --volume="$PWD:$PWD" --user="$(id --user):$(id --group)" --workdir="$PWD" --env=HOME --entrypoint=make --rm -it ghcr.io/pgaskin/nickeltc:1.0 all koboroot`. To compile it on the host, use `make CROSS_COMPILE=/path/to/nickeltc/bin/arm-nickel-linux-gnueabihf-`. 23 | 24 | 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | tag_name: 7 | description: 'new/existing tag name' 8 | required: true 9 | target_commitish: 10 | description: 'target if new tag' 11 | default: master 12 | 13 | permissions: 14 | contents: write 15 | id-token: write 16 | attestations: write 17 | 18 | jobs: 19 | build: 20 | name: NickelMenu 21 | runs-on: ubuntu-latest 22 | container: ghcr.io/pgaskin/nickeltc:1.0 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v6 26 | with: 27 | submodules: true 28 | - name: Build 29 | uses: docker://ghcr.io/pgaskin/nickeltc:1.0 30 | with: 31 | entrypoint: make 32 | args: all koboroot 33 | - name: Attest 34 | uses: actions/attest-build-provenance@v3 35 | with: 36 | subject-path: | 37 | KoboRoot.tgz 38 | - name: Create draft release 39 | uses: actions/github-script@v8 40 | id: draft_release 41 | with: 42 | script: | 43 | const {data: {id: id}} = await github.rest.repos.createRelease({ 44 | owner: context.repo.owner, 45 | repo: context.repo.repo, 46 | tag_name: context.payload.inputs.tag_name, 47 | target_commitish: context.payload.inputs.target_commitish, 48 | name: `NickelMenu ${context.payload.inputs.tag_name}`, 49 | draft: true, 50 | }) 51 | core.setOutput('id', id) 52 | - name: Upload release asset 53 | uses: actions/github-script@v8 54 | with: 55 | retries: 3 # note: this applies to individual github.rest calls, not the entire script 56 | script: | 57 | const {readFile} = require('fs').promises 58 | await github.rest.repos.uploadReleaseAsset({ 59 | owner: context.repo.owner, 60 | repo: context.repo.repo, 61 | release_id: '${{steps.draft_release.outputs.id}}', 62 | name: 'KoboRoot.tgz', 63 | data: await readFile(`KoboRoot.tgz`), 64 | }) 65 | -------------------------------------------------------------------------------- /src/action.h: -------------------------------------------------------------------------------- 1 | #ifndef NM_ACTION_H 2 | #define NM_ACTION_H 3 | #ifdef __cplusplus 4 | extern "C" { 5 | #endif 6 | 7 | typedef enum { 8 | NM_ACTION_RESULT_TYPE_SILENT = 0, 9 | NM_ACTION_RESULT_TYPE_MSG = 1, 10 | NM_ACTION_RESULT_TYPE_TOAST = 2, 11 | NM_ACTION_RESULT_TYPE_SKIP = 3, // for use by skip only 12 | } nm_action_result_type_t; 13 | 14 | typedef struct { 15 | nm_action_result_type_t type; 16 | char *msg; 17 | int skip; // for use by skip only 18 | } nm_action_result_t; 19 | 20 | // nm_action_fn_t represents an action. On success, a nm_action_result_t is 21 | // returned and needs to be freed with nm_action_result_free. Otherwise, NULL is 22 | // returned and nm_err is set. 23 | typedef nm_action_result_t *(*nm_action_fn_t)(const char *arg); 24 | 25 | nm_action_result_t *nm_action_result_silent(); 26 | nm_action_result_t *nm_action_result_msg(const char *fmt, ...) __attribute__((format(printf, 1, 2))); 27 | nm_action_result_t *nm_action_result_toast(const char *fmt, ...) __attribute__((format(printf, 1, 2))); 28 | void nm_action_result_free(nm_action_result_t *res); 29 | 30 | #define NM_ACTION(name) nm_action_##name 31 | 32 | #ifdef __cplusplus 33 | #define NM_ACTION_(name) extern "C" nm_action_result_t *NM_ACTION(name)(const char *arg) 34 | #else 35 | #define NM_ACTION_(name) nm_action_result_t *NM_ACTION(name)(const char *arg) 36 | #endif 37 | 38 | #define NM_ACTIONS \ 39 | X(cmd_spawn) \ 40 | X(cmd_output) \ 41 | X(dbg_syslog) \ 42 | X(dbg_error) \ 43 | X(dbg_msg) \ 44 | X(dbg_toast) \ 45 | X(kfmon) \ 46 | X(kfmon_id) \ 47 | X(nickel_setting) \ 48 | X(nickel_extras) \ 49 | X(nickel_browser) \ 50 | X(nickel_misc) \ 51 | X(nickel_open) \ 52 | X(nickel_wifi) \ 53 | X(nickel_bluetooth) \ 54 | X(nickel_orientation) \ 55 | X(nickel_screenshot) \ 56 | X(power) \ 57 | X(skip) \ 58 | X(uninstall) 59 | 60 | #define X(name) NM_ACTION_(name); 61 | NM_ACTIONS 62 | #undef X 63 | 64 | #ifdef __cplusplus 65 | } 66 | #endif 67 | #endif 68 | -------------------------------------------------------------------------------- /src/util.h: -------------------------------------------------------------------------------- 1 | #ifndef NM_UTIL_H 2 | #define NM_UTIL_H 3 | #ifdef __cplusplus 4 | extern "C" { 5 | #endif 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | 14 | // strtrim trims ASCII whitespace in-place (i.e. don't give it a string literal) 15 | // from the left/right of the string. 16 | __attribute__((unused)) static inline char *strtrim(char *s) { 17 | if (!s) return NULL; 18 | char *a = s, *b = s + strlen(s); 19 | for (; a < b && isspace((unsigned char)(*a)); a++); 20 | for (; b > a && isspace((unsigned char)(*(b-1))); b--); 21 | *b = '\0'; 22 | return a; 23 | } 24 | 25 | // NM_LOG writes a log message. 26 | #define NM_LOG(fmt, ...) nh_log(fmt " (%s:%d)", ##__VA_ARGS__, __FILE__, __LINE__) 27 | 28 | // Error handling (thread-safe): 29 | 30 | // nm_err returns the current error message and clears the error state. If there 31 | // isn't any error set, NULL is returned. The returned string is only valid on 32 | // the current thread until nm_err_set is called. 33 | const char *nm_err(); 34 | 35 | // nm_err_peek is like nm_err, but doesn't clear the error state. 36 | const char *nm_err_peek(); 37 | 38 | // nm_err_set sets the current error message to the specified format string. If 39 | // fmt is NULL, the error is cleared. It is safe to use the return value of 40 | // nm_err as an argument. If fmt was not NULL, true is returned. 41 | bool nm_err_set(const char *fmt, ...) __attribute__((format(printf, 1, 2))); 42 | 43 | // NM_ERR_SET set is like nm_err_set, but also includes information about the 44 | // current file/line. To set it to NULL, use nm_err_set directly. 45 | #define NM_ERR_SET(fmt, ...) \ 46 | nm_err_set((fmt " (%s:%d)"), ##__VA_ARGS__, __FILE__, __LINE__); 47 | 48 | // NM_ERR_RET is like NM_ERR_SET, but also returns the specified value. 49 | #define NM_ERR_RET(ret, fmt, ...) do { \ 50 | NM_ERR_SET(fmt, ##__VA_ARGS__); \ 51 | return (ret); \ 52 | } while (0) 53 | 54 | // NM_CHECK checks a condition and calls nm_err_set then returns the specified 55 | // value if the condition is false. Otherwise, nothing happens. 56 | #define NM_CHECK(ret, cond, fmt, ...) do { \ 57 | if (!(cond)) { \ 58 | nm_err_set((fmt " (check failed: %s)"), ##__VA_ARGS__, #cond); \ 59 | return (ret); \ 60 | } \ 61 | } while (0) 62 | 63 | #ifdef __cplusplus 64 | } 65 | #endif 66 | #endif 67 | -------------------------------------------------------------------------------- /src/generator.c: -------------------------------------------------------------------------------- 1 | #define _GNU_SOURCE // asprintf 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "action.h" 8 | #include "generator.h" 9 | #include "nickelmenu.h" 10 | #include "util.h" 11 | 12 | nm_menu_item_t **nm_generator_do(nm_generator_t *gen, size_t *sz_out) { 13 | NM_LOG("generator: running generator (%s) (%s) (%d) (%p)", gen->desc, gen->arg, gen->loc, gen->generate); 14 | 15 | struct timespec old = gen->time; 16 | size_t sz = (size_t)(-1); // this should always be set by generate upon success, but we'll initialize it just in case 17 | nm_menu_item_t **items = gen->generate(gen->arg, &gen->time, &sz); 18 | 19 | if (items && old.tv_sec == gen->time.tv_sec && old.tv_nsec == gen->time.tv_nsec) 20 | NM_LOG("generator: bug: new items were returned, but time wasn't changed"); 21 | 22 | const char *err = nm_err(); 23 | 24 | if (!old.tv_sec && !old.tv_nsec && !err && !items) 25 | NM_LOG("generator: warning: no existing items (time == 0), but no new items or error were returned"); 26 | 27 | if (err) { 28 | if (items) 29 | NM_LOG("generator: bug: items should be null on error"); 30 | 31 | NM_LOG("generator: generator error (%s) (%s), replacing with error item: %s", gen->desc, gen->arg, err); 32 | sz = 1; 33 | items = calloc(sz, sizeof(nm_menu_item_t*)); 34 | items[0] = calloc(1, sizeof(nm_menu_item_t)); 35 | // loc will be set below 36 | items[0]->lbl = strdup("Generator error"); 37 | items[0]->action = calloc(1, sizeof(nm_menu_action_t)); 38 | items[0]->action->act = NM_ACTION(dbg_msg); 39 | asprintf(&items[0]->action->arg, "%s: %s", gen->desc, err); 40 | items[0]->action->on_failure = true; 41 | items[0]->action->on_success = true; 42 | } 43 | 44 | if (!err && !items && (old.tv_sec != gen->time.tv_sec || old.tv_nsec != gen->time.tv_nsec)) 45 | NM_LOG("generator: bug: the time should have been updated if new items were returned"); 46 | 47 | if (items) { 48 | if (sz == (size_t)(-1)) 49 | NM_LOG("generator: bug: size should have been set by generate, but wasn't"); 50 | if (!sz) 51 | NM_LOG("generator: bug: items should be null when size is 0"); 52 | 53 | for (size_t i = 0; i < sz; i++) { 54 | if (items[i]->loc) 55 | NM_LOG("generator: bug: generator should not set the menu item location, as it will be overridden"); 56 | 57 | items[i]->loc = gen->loc; 58 | } 59 | } 60 | 61 | *sz_out = sz; 62 | return items; 63 | } 64 | -------------------------------------------------------------------------------- /src/generator.h: -------------------------------------------------------------------------------- 1 | #ifndef NM_GENERATOR_H 2 | #define NM_GENERATOR_H 3 | #ifdef __cplusplus 4 | extern "C" { 5 | #endif 6 | 7 | #include 8 | #include 9 | #include "nickelmenu.h" 10 | 11 | // nm_generator_fn_t generates menu items. It must return a malloc'd array of 12 | // pointers to malloc'd nm_menu_item_t's, and write the number of items to 13 | // out_sz. The menu item locations must not be set. On error, nm_err must be 14 | // set, NULL must be returned, and sz_out is undefined. If no entries are 15 | // generated, NULL must be returned with sz_out set to 0. All strings should 16 | // also be malloc'd. On success, nm_err must be cleared. 17 | // 18 | // time_in_out will not be NULL, and contains zero or the last modification time 19 | // for the generator. If it is zero, the generator should generate the items as 20 | // usual and update the time. If it is nonzero, the generator should return NULL 21 | // without making changes if the time is up to date (the check should be as 22 | // quick as possible), and if not, it should update the items and update the 23 | // time to match. If the generator does not have a way of checking for updates 24 | // quickly, it should only update the item and set the time to a nonzero value 25 | // if the time is zero, and return NULL if the time is nonzero. Note that this 26 | // time doesn't have to account for different arguments or multiple instances, 27 | // as changes in those will always cause the time to be set to zero. 28 | typedef nm_menu_item_t **(*nm_generator_fn_t)(const char *arg, struct timespec *time_in_out, size_t *sz_out); 29 | 30 | typedef struct { 31 | char *desc; // only used for making the errors more meaningful (it is the title) 32 | char *arg; 33 | nm_menu_location_t loc; 34 | nm_generator_fn_t generate; // should be as quick as possible with a short timeout, as it will block startup 35 | struct timespec time; 36 | } nm_generator_t; 37 | 38 | // nm_generator_do runs a generator and returns the generated items, if any, or 39 | // an item which shows the error returned by the generator. If NULL is returned, 40 | // no items needed to be updated (set time to zero to force an update) (sz_out 41 | // is undefined). 42 | nm_menu_item_t **nm_generator_do(nm_generator_t *gen, size_t *sz_out); 43 | 44 | #define NM_GENERATOR(name) nm_generator_##name 45 | 46 | #ifdef __cplusplus 47 | #define NM_GENERATOR_(name) extern "C" nm_menu_item_t **NM_GENERATOR(name)(const char *arg, struct timespec *time_in_out, size_t *sz_out) 48 | #else 49 | #define NM_GENERATOR_(name) nm_menu_item_t **NM_GENERATOR(name)(const char *arg, struct timespec *time_in_out, size_t *sz_out) 50 | #endif 51 | 52 | #define NM_GENERATORS \ 53 | X(_test) \ 54 | X(_test_time) \ 55 | X(kfmon) 56 | 57 | #define X(name) NM_GENERATOR_(name); 58 | NM_GENERATORS 59 | #undef X 60 | 61 | #ifdef __cplusplus 62 | } 63 | #endif 64 | #endif 65 | -------------------------------------------------------------------------------- /src/kfmon.h: -------------------------------------------------------------------------------- 1 | #ifndef NM_KFMON_H 2 | #define NM_KFMON_H 3 | #ifdef __cplusplus 4 | extern "C" { 5 | #endif 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include "action.h" 12 | 13 | // Path to KFMon's IPC Unix socket 14 | #define KFMON_IPC_SOCKET "/tmp/kfmon-ipc.ctl" 15 | 16 | // Flags for the failure bingo 17 | typedef enum { 18 | // Not an error ;p 19 | KFMON_IPC_OK = EXIT_SUCCESS, 20 | // NOTE: Start > 256 to stay clear of errno 21 | KFMON_IPC_ETIMEDOUT = 512, 22 | KFMON_IPC_EPIPE, 23 | KFMON_IPC_ENODATA, 24 | // syscall failures 25 | KFMON_IPC_READ_FAILURE, 26 | KFMON_IPC_SEND_FAILURE, 27 | KFMON_IPC_SOCKET_FAILURE, 28 | KFMON_IPC_CONNECT_FAILURE, 29 | KFMON_IPC_POLL_FAILURE, 30 | KFMON_IPC_CALLOC_FAILURE, 31 | KFMON_IPC_REPLY_READ_FAILURE, 32 | KFMON_IPC_LIST_PARSE_FAILURE, 33 | // Those match the actual string sent over the wire 34 | KFMON_IPC_ERR_INVALID_ID, 35 | KFMON_IPC_ERR_INVALID_NAME, 36 | KFMON_IPC_WARN_ALREADY_RUNNING, 37 | KFMON_IPC_WARN_SPAWN_BLOCKED, 38 | KFMON_IPC_WARN_SPAWN_INHIBITED, 39 | KFMON_IPC_ERR_REALLY_MALFORMED_CMD, 40 | KFMON_IPC_ERR_MALFORMED_CMD, 41 | KFMON_IPC_ERR_INVALID_CMD, 42 | KFMON_IPC_UNKNOWN_REPLY, 43 | // Not an error either, means we have more to read... 44 | KFMON_IPC_EAGAIN, 45 | } kfmon_ipc_errno_e; 46 | 47 | // A single watch item 48 | typedef struct { 49 | uint8_t idx; 50 | char *filename; 51 | char *label; 52 | } kfmon_watch_t; 53 | 54 | // A node in a linked list of watches 55 | typedef struct kfmon_watch_node { 56 | kfmon_watch_t watch; 57 | struct kfmon_watch_node *next; 58 | } kfmon_watch_node_t; 59 | 60 | // A control structure to keep track of a list of watches 61 | typedef struct { 62 | size_t count; 63 | kfmon_watch_node_t *head; 64 | kfmon_watch_node_t *tail; 65 | } kfmon_watch_list_t; 66 | 67 | // Used as the reply handler in our polling loops. 68 | // Second argument is an opaque pointer used for storage in a linked list 69 | // (e.g., a pointer to a kfmon_watch_list_t, or NULL if no storage is needed). 70 | typedef int (*ipc_handler_t)(int, void *); 71 | 72 | // Free all resources allocated by a list and its nodes 73 | void kfmon_teardown_list(kfmon_watch_list_t *list); 74 | // Allocate a single new node to the list 75 | int kfmon_grow_list(kfmon_watch_list_t *list); 76 | 77 | // If status is success, false is returned. Otherwise, true is returned and 78 | // nm_err is set. 79 | bool nm_kfmon_error_handler(kfmon_ipc_errno_e status); 80 | 81 | // Given one of the error codes listed above, return properly from an action. 82 | nm_action_result_t *nm_kfmon_return_handler(kfmon_ipc_errno_e status); 83 | 84 | // Send a simple KFMon IPC request, one where the reply is only used for its diagnostic value. 85 | int nm_kfmon_simple_request(const char *restrict ipc_cmd, const char *restrict ipc_arg); 86 | 87 | // Handle a list request for the KFMon generator 88 | int nm_kfmon_list_request(const char *restrict ipc_cmd, kfmon_watch_list_t *list); 89 | 90 | #ifdef __cplusplus 91 | } 92 | #endif 93 | #endif 94 | -------------------------------------------------------------------------------- /src/action_c.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "action.h" 10 | #include "kfmon.h" 11 | #include "util.h" 12 | 13 | NM_ACTION_(dbg_syslog) { 14 | NM_LOG("dbgsyslog: %s", arg); 15 | return nm_action_result_silent(); 16 | } 17 | 18 | NM_ACTION_(dbg_error) { 19 | NM_ERR_SET("%s", arg); 20 | return NULL; 21 | } 22 | 23 | NM_ACTION_(dbg_msg) { 24 | return nm_action_result_msg("%s", arg); 25 | } 26 | 27 | NM_ACTION_(dbg_toast) { 28 | return nm_action_result_toast("%s", arg); 29 | } 30 | 31 | NM_ACTION_(skip) { 32 | char *tmp; 33 | long n = strtol(arg, &tmp, 10); 34 | NM_CHECK(NULL, *arg && !*tmp && n != 0 && n >= -1 && n < INT_MAX, "invalid count '%s': must be a nonzero integer or -1", arg); 35 | 36 | nm_action_result_t *res = calloc(1, sizeof(nm_action_result_t)); 37 | res->type = NM_ACTION_RESULT_TYPE_SKIP; 38 | res->skip = (int)(n); 39 | return res; 40 | } 41 | 42 | NM_ACTION_(kfmon_id) { 43 | // Start by watch ID (simpler, but IDs may not be stable across a single power cycle, given severe KFMon config shuffling) 44 | int status = nm_kfmon_simple_request("start", arg); 45 | return nm_kfmon_return_handler(status); 46 | } 47 | 48 | NM_ACTION_(kfmon) { 49 | // Trigger a watch, given its trigger basename. Stable runtime lookup done by KFMon. 50 | int status = nm_kfmon_simple_request("trigger", arg); 51 | 52 | // Fixup INVALID_ID to INVALID_NAME for slightly clearer feedback (see e8b2588 for details). 53 | if (status == KFMON_IPC_ERR_INVALID_ID) 54 | status = KFMON_IPC_ERR_INVALID_NAME; 55 | 56 | return nm_kfmon_return_handler(status); 57 | } 58 | 59 | NM_ACTION_(uninstall) { 60 | (void) arg; 61 | if (remove("/usr/local/Kobo/imageformats/libnm.so")) { // TODO: don't hardcode this 62 | NM_LOG("uninstall: failed to remove library: %s", strerror(errno)); 63 | NM_LOG("uninstall: falling back to uninstall flag"); 64 | if (mkdir("/mnt/onboard/.adds", 0755) && errno != EEXIST) // TODO: don't hardcode this 65 | return nm_action_result_msg("failed to create uninstall flag: mkdir .adds: %s", strerror(errno)); 66 | if (mkdir(NM_CONFIG_DIR, 0755) && errno != EEXIST) 67 | return nm_action_result_msg("failed to create uninstall flag: mkdir %s: %s", NM_CONFIG_DIR, strerror(errno)); 68 | int fd = open(NM_CONFIG_DIR "/uninstall", O_CREAT|O_WRONLY, 0644); 69 | if (fd == -1) { 70 | return nm_action_result_msg("failed to create uninstall flag: open %s/uninstall: %s", NM_CONFIG_DIR, strerror(errno)); 71 | } 72 | NM_LOG("uninstall: created uninstall flag"); 73 | close(fd); 74 | } else { 75 | NM_LOG("uninstall: removed lib"); 76 | if (!remove(NM_CONFIG_DIR "/uninstall")) // so if the user created it already, it won't uninstall the next time it gets installed 77 | NM_LOG("uninstall: removed old uninstall flag"); 78 | } 79 | NM_LOG("uninstall: rebooting"); 80 | return nm_action_power("reboot"); 81 | } 82 | -------------------------------------------------------------------------------- /src/config.h: -------------------------------------------------------------------------------- 1 | #ifndef NM_CONFIG_H 2 | #define NM_CONFIG_H 3 | #ifdef __cplusplus 4 | extern "C" { 5 | #endif 6 | 7 | #include 8 | #include 9 | 10 | #include "action.h" 11 | #include "nickelmenu.h" 12 | 13 | #if !(defined(NM_CONFIG_DIR) && defined(NM_CONFIG_DIR_DISP)) 14 | #error "NM_CONFIG_DIR not set (it should be done by the Makefile)" 15 | #endif 16 | 17 | #ifndef NM_CONFIG_MAX_MENU_ITEMS_PER_MENU 18 | #define NM_CONFIG_MAX_MENU_ITEMS_PER_MENU 50 19 | #endif 20 | 21 | typedef struct nm_config_t nm_config_t; 22 | 23 | typedef struct nm_config_file_t nm_config_file_t; 24 | 25 | // nm_config_parse lists the configuration files in NM_CONFIG_DIR. If there are 26 | // errors reading the dir, NULL is returned and nm_err is set. 27 | nm_config_file_t *nm_config_files(); 28 | 29 | // nm_config_files_update checks if the configuration files are up to date and 30 | // updates them. If the files are already up-to-date, 1 is returned. If the 31 | // files were updated, 0 is returned. If an error occurs, the pointer is left 32 | // untouched and -1 is returned with nm_err set. Warning: if the files have 33 | // changed, the pointer passed to files will become invalid (it gets replaced). 34 | // If *files is NULL, it is equivalent to doing `*files = nm_config_files()`. 35 | int nm_config_files_update(nm_config_file_t **files); 36 | 37 | // nm_config_files_free frees the list of configuration files. 38 | void nm_config_files_free(nm_config_file_t *files); 39 | 40 | // nm_config_parse parses the configuration files. If there are syntax errors, 41 | // file access errors, or invalid action names for menu_item, then NULL is 42 | // returned and nm_err is set. On success, the config is returned. 43 | nm_config_t *nm_config_parse(nm_config_file_t *files); 44 | 45 | // nm_config_generate runs all generators synchronously and sequentially. Any 46 | // previously generated items are automatically removed if updates are required. 47 | // If the config was modified, true is returned. 48 | bool nm_config_generate(nm_config_t *cfg, bool force_update); 49 | 50 | // nm_config_get_menu gets a malloc'd array of pointers to the menu items 51 | // defined in the config. These pointers will be valid until nm_config_free is 52 | // called. 53 | nm_menu_item_t **nm_config_get_menu(nm_config_t *cfg, size_t *n_out); 54 | 55 | // nm_config_experimental gets the first value of an arbitrary experimental 56 | // option. If it doesn't exist, NULL will be returned. The pointer will be valid 57 | // until nm_config_free is called. 58 | const char *nm_config_experimental(nm_config_t *cfg, const char *key); 59 | 60 | // nm_config_free frees all allocated memory. 61 | void nm_config_free(nm_config_t *cfg); 62 | 63 | // nm_global_config_update updates and regenerates the config if needed. If the 64 | // menu items changed (i.e. the old items aren't valid anymore), the revision 65 | // will be incremented and returned (even if there was an error). On error, 66 | // nm_err is set, and otherwise, it is cleared. 67 | int nm_global_config_update(); 68 | 69 | // nm_global_config_items returns an array of pointers with the current menu 70 | // items (the pointer and the items it points to will remain valid until the 71 | // next time nm_global_config_update is called). The number of items is stored 72 | // in the variable pointed to by n_out. If an error ocurred during the last time 73 | // nm_global_config_update was called, it is returned as a "Config Error" menu 74 | // item. If nm_global_config_update has never been called successfully before, 75 | // NULL is returned and n_out is set to 0. 76 | nm_menu_item_t **nm_global_config_items(size_t *n_out); 77 | 78 | // nm_global_config_experimental gets the first value of an arbitrary 79 | // experimental option (the pointer will remain valid until the next time 80 | // nm_global_config_update is called). If it doesn't exist, NULL will be 81 | // returned. 82 | const char *nm_global_config_experimental(const char *key); 83 | 84 | #ifdef __cplusplus 85 | } 86 | #endif 87 | #endif 88 | -------------------------------------------------------------------------------- /src/generator_c.c: -------------------------------------------------------------------------------- 1 | #define _GNU_SOURCE // asprintf 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "generator.h" 12 | #include "kfmon.h" 13 | #include "nickelmenu.h" 14 | #include "util.h" 15 | 16 | NM_GENERATOR_(_test) { 17 | if (time_in_out->tv_sec || time_in_out->tv_nsec) { 18 | nm_err_set(NULL); 19 | return NULL; // updates not supported (or needed, for that matter) 20 | } 21 | 22 | char *tmp; 23 | long n = strtol(arg, &tmp, 10); 24 | NM_CHECK(NULL, *arg && !*tmp && n >= 0 && n <= 10, "invalid count '%s': must be an integer from 1-10", arg); 25 | 26 | if (n == 0) { 27 | *sz_out = 0; 28 | nm_err_set(NULL); 29 | return NULL; 30 | } 31 | 32 | nm_menu_item_t **items = calloc(n, sizeof(nm_menu_item_t*)); 33 | for (size_t i = 0; i < (size_t)(n); i++) { 34 | items[i] = calloc(1, sizeof(nm_menu_item_t)); 35 | items[i]->action = calloc(1, sizeof(nm_menu_action_t)); 36 | asprintf(&items[i]->lbl, "Generated %zu", i+1); 37 | items[i]->action->act = NM_ACTION(dbg_msg); 38 | items[i]->action->arg = strdup("Pressed"); 39 | items[i]->action->on_failure = true; 40 | items[i]->action->on_success = true; 41 | } 42 | 43 | clock_gettime(CLOCK_REALTIME, time_in_out); // note: any nonzero value would work, but this generator is for testing and as an example 44 | 45 | *sz_out = n; 46 | nm_err_set(NULL); 47 | return items; 48 | } 49 | 50 | NM_GENERATOR_(_test_time) { 51 | if (arg && *arg) 52 | NM_ERR_RET(NULL, "_test_time does not accept any arguments"); 53 | 54 | // note: this used as an example and for testing 55 | 56 | NM_LOG("_test_time: checking for updates"); 57 | 58 | struct timespec ts; 59 | clock_gettime(CLOCK_REALTIME, &ts); 60 | 61 | struct tm lt; 62 | localtime_r(&ts.tv_sec, <); 63 | 64 | if (time_in_out->tv_sec && ts.tv_sec - time_in_out->tv_sec < 10) { 65 | NM_LOG("_test_time: last update is nonzero and last update time is < 10s, skipping"); 66 | nm_err_set(NULL); 67 | return NULL; 68 | } 69 | 70 | NM_LOG("_test_time: updating"); 71 | 72 | // note: you'd usually do the slower logic here 73 | 74 | nm_menu_item_t **items = calloc(1, sizeof(nm_menu_item_t*)); 75 | items[0] = calloc(1, sizeof(nm_menu_item_t)); 76 | asprintf(&items[0]->lbl, "%d:%02d:%02d", lt.tm_hour, lt.tm_min, lt.tm_sec); 77 | items[0]->action = calloc(1, sizeof(nm_menu_action_t)); 78 | items[0]->action->act = NM_ACTION(dbg_msg); 79 | items[0]->action->arg = strdup("It worked!"); 80 | items[0]->action->on_failure = true; 81 | items[0]->action->on_success = true; 82 | 83 | time_in_out->tv_sec = ts.tv_sec; 84 | 85 | *sz_out = 1; 86 | nm_err_set(NULL); 87 | return items; 88 | } 89 | 90 | NM_GENERATOR_(kfmon) { 91 | struct stat sb; 92 | if (stat(KFMON_IPC_SOCKET, &sb)) 93 | NM_ERR_RET(NULL, "error checking '%s': stat: %m", KFMON_IPC_SOCKET); 94 | 95 | if (time_in_out->tv_sec == sb.st_mtim.tv_sec && time_in_out->tv_nsec == sb.st_mtim.tv_nsec) { 96 | nm_err_set(NULL); 97 | return NULL; 98 | } 99 | 100 | // Default with no arg or an empty arg is to request a gui-listing 101 | const char *kfmon_cmd = NULL; 102 | if (!arg || !*arg || !strcmp(arg, "gui")) { 103 | kfmon_cmd = "gui-list"; 104 | } else if (!strcmp(arg, "all")) { 105 | kfmon_cmd = "list"; 106 | } else { 107 | NM_ERR_RET(NULL, "invalid argument '%s': if specified, must be either gui or all", arg); 108 | } 109 | 110 | // We'll want to retrieve our watch list in there. 111 | kfmon_watch_list_t list = { 0 }; 112 | int status = nm_kfmon_list_request(kfmon_cmd, &list); 113 | 114 | // If there was an error, handle it now. 115 | if (nm_kfmon_error_handler(status)) 116 | return NULL; // the error will be passed on 117 | 118 | // Handle an empty listing safely 119 | if (list.count == 0) { 120 | *sz_out = 0; 121 | nm_err_set(NULL); 122 | return NULL; 123 | } 124 | 125 | // And now we can start populating an array of nm_menu_item_t :) 126 | *sz_out = list.count; 127 | nm_menu_item_t **items = calloc(list.count, sizeof(nm_menu_item_t*)); 128 | 129 | // Walk the list to populate the items array 130 | size_t i = 0; 131 | for (kfmon_watch_node_t *node = list.head; node != NULL; node = node->next) { 132 | items[i] = calloc(1, sizeof(nm_menu_item_t)); 133 | items[i]->action = calloc(1, sizeof(nm_menu_action_t)); 134 | items[i]->lbl = strdup(node->watch.label); 135 | items[i]->action->act = NM_ACTION(kfmon); 136 | items[i]->action->arg = strdup(node->watch.filename); 137 | items[i]->action->on_failure = true; 138 | items[i]->action->on_success = true; 139 | i++; 140 | } 141 | 142 | // Destroy the list now that we've dumped it into an array of nm_menu_item_t 143 | kfmon_teardown_list(&list); 144 | 145 | *time_in_out = sb.st_mtim; 146 | nm_err_set(NULL); 147 | return items; 148 | } 149 | -------------------------------------------------------------------------------- /src/kfmon_helpers.h: -------------------------------------------------------------------------------- 1 | /* $OpenBSD: atomicio.c,v 1.30 2019/01/24 02:42:23 dtucker Exp $ */ 2 | /* 3 | * Copyright (c) 2006 Damien Miller. All rights reserved. 4 | * Copyright (c) 2005 Anil Madhavapeddy. All rights reserved. 5 | * Copyright (c) 1995,1999 Theo de Raadt. All rights reserved. 6 | * All rights reserved. 7 | * SPDX-License-Identifier: BSD-2-Clause 8 | * 9 | * Redistribution and use in source and binary forms, with or without 10 | * modification, are permitted provided that the following conditions 11 | * are met: 12 | * 1. Redistributions of source code must retain the above copyright 13 | * notice, this list of conditions and the following disclaimer. 14 | * 2. Redistributions in binary form must reproduce the above copyright 15 | * notice, this list of conditions and the following disclaimer in the 16 | * documentation and/or other materials provided with the distribution. 17 | * 18 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 19 | * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 20 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 21 | * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 22 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 23 | * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 27 | * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | */ 29 | 30 | // NOTE: Originally imported from https://github.com/openssh/openssh-portable/blob/master/atomicio.c 31 | // Rejigged for my own use, with inspiration from git's own read/write wrappers, 32 | // as well as gnulib's and busybox's 33 | // c.f., https://github.com/git/git/blob/master/wrapper.c 34 | // https://git.savannah.gnu.org/cgit/gnulib.git/tree/lib/safe-read.c 35 | // https://git.savannah.gnu.org/cgit/gnulib.git/tree/lib/full-write.c 36 | // https://git.busybox.net/busybox/tree/libbb/read.c 37 | 38 | #ifndef NM_KFMON_HELPERS_H 39 | #define NM_KFMON_HELPERS_H 40 | #ifdef __cplusplus 41 | extern "C" { 42 | #endif 43 | 44 | #include 45 | #include 46 | #include 47 | #include 48 | #include 49 | #include 50 | 51 | // Clamp IO chunks to the smallest of 8 MiB and SSIZE_MAX, 52 | // to deal with various implementation quirks on really old Linux, 53 | // macOS, or AIX/IRIX. 54 | // c.f., git, gnulib & busybox for similar stories. 55 | // Since we ourselves are 32 bit Linux-bound, 8 MiB suits us just fine. 56 | #define MAX_IO_BUFSIZ (8 * 1024 * 1024) 57 | #if defined(SSIZE_MAX) && (SSIZE_MAX < MAX_IO_BUFSIZ) 58 | # undef MAX_IO_BUFSIZ 59 | # define MAX_IO_BUFSIZ SSIZE_MAX 60 | #endif 61 | 62 | // read() with retries on recoverable errors (via polling on EAGAIN). 63 | // Not guaranteed to return len bytes, even on success (like read() itself). 64 | // Always returns read()'s return value as-is. 65 | static ssize_t xread(int fd, void *buf, size_t len) { 66 | // Save a trip to EINVAL if len is large enough to make read() fail. 67 | if (len > MAX_IO_BUFSIZ) { 68 | len = MAX_IO_BUFSIZ; 69 | } 70 | 71 | while (1) { 72 | ssize_t nr = read(fd, buf, len); 73 | if (nr < 0) { 74 | if (errno == EINTR) { 75 | continue; 76 | } else if (errno == EAGAIN) { 77 | struct pollfd pfd = { 0 }; 78 | pfd.fd = fd; 79 | pfd.events = POLLIN; 80 | 81 | poll(&pfd, 1, -1); 82 | continue; 83 | } 84 | } 85 | return nr; 86 | } 87 | } 88 | 89 | // write() with retries on recoverable errors (via polling on EAGAIN). 90 | // Not guaranteed to write len bytes, even on success (like write() itself). 91 | // Always returns write()'s return value as-is. 92 | static __attribute__((unused)) ssize_t xwrite(int fd, const void *buf, size_t len) { 93 | // Save a trip to EINVAL if len is large enough to make write() fail. 94 | if (len > MAX_IO_BUFSIZ) { 95 | len = MAX_IO_BUFSIZ; 96 | } 97 | 98 | while (1) { 99 | ssize_t nw = write(fd, buf, len); 100 | if (nw < 0) { 101 | if (errno == EINTR) { 102 | continue; 103 | } else if (errno == EAGAIN) { 104 | struct pollfd pfd = { 0 }; 105 | pfd.fd = fd; 106 | pfd.events = POLLOUT; 107 | 108 | poll(&pfd, 1, -1); 109 | continue; 110 | } 111 | } 112 | return nw; 113 | } 114 | } 115 | 116 | // Based on OpenSSH's atomicio6, except we keep the return value/data type of the original call. 117 | // Ensure all of data on socket comes through. 118 | static __attribute__((unused)) ssize_t read_in_full(int fd, void *buf, size_t len) { 119 | // Save a trip to EINVAL if len is large enough to make read() fail. 120 | if (len > MAX_IO_BUFSIZ) { 121 | len = MAX_IO_BUFSIZ; 122 | } 123 | 124 | char *s = buf; 125 | size_t pos = 0U; 126 | while (len > pos) { 127 | ssize_t nr = read(fd, s + pos, len - pos); 128 | switch (nr) { 129 | case -1: 130 | if (errno == EINTR) { 131 | continue; 132 | } else if (errno == EAGAIN) { 133 | struct pollfd pfd = { 0 }; 134 | pfd.fd = fd; 135 | pfd.events = POLLIN; 136 | 137 | poll(&pfd, 1, -1); 138 | continue; 139 | } 140 | return -1; 141 | case 0: 142 | // i.e., EoF/EoT 143 | errno = EPIPE; 144 | return (ssize_t) pos; 145 | default: 146 | pos += (size_t) nr; 147 | } 148 | } 149 | return (ssize_t) pos; 150 | } 151 | 152 | static __attribute__((unused)) ssize_t write_in_full(int fd, const void *buf, size_t len) { 153 | // Save a trip to EINVAL if len is large enough to make write() fail. 154 | if (len > MAX_IO_BUFSIZ) { 155 | len = MAX_IO_BUFSIZ; 156 | } 157 | 158 | const char *s = buf; 159 | size_t pos = 0U; 160 | while (len > pos) { 161 | ssize_t nw = write(fd, s + pos, len - pos); 162 | switch (nw) { 163 | case -1: 164 | if (errno == EINTR) { 165 | continue; 166 | } else if (errno == EAGAIN) { 167 | struct pollfd pfd = { 0 }; 168 | pfd.fd = fd; 169 | pfd.events = POLLOUT; 170 | 171 | poll(&pfd, 1, -1); 172 | continue; 173 | } 174 | return -1; 175 | case 0: 176 | // That only makes sense for regular files. 177 | // On the other hand, write() returning 0 on !regular files is UB. 178 | errno = ENOSPC; 179 | return -1; 180 | default: 181 | pos += (size_t) nw; 182 | } 183 | } 184 | return (ssize_t) pos; 185 | } 186 | 187 | // Exactly like write_in_full, but using send w/ flags set to MSG_NOSIGNAL, 188 | // so we can handle EPIPE without having to deal with signals. 189 | static ssize_t send_in_full(int sockfd, const void *buf, size_t len) { 190 | // Save a trip to EINVAL if len is large enough to make send() fail. 191 | if (len > MAX_IO_BUFSIZ) { 192 | len = MAX_IO_BUFSIZ; 193 | } 194 | 195 | const char *s = buf; 196 | size_t pos = 0U; 197 | while (len > pos) { 198 | ssize_t nw = send(sockfd, s + pos, len - pos, MSG_NOSIGNAL); 199 | switch (nw) { 200 | case -1: 201 | if (errno == EINTR) { 202 | continue; 203 | } else if (errno == EAGAIN) { 204 | struct pollfd pfd = { 0 }; 205 | pfd.fd = sockfd; 206 | pfd.events = POLLOUT; 207 | 208 | poll(&pfd, 1, -1); 209 | continue; 210 | } 211 | return -1; 212 | default: 213 | pos += (size_t) nw; 214 | } 215 | } 216 | return (ssize_t) pos; 217 | } 218 | 219 | #ifdef __cplusplus 220 | } 221 | #endif 222 | #endif 223 | -------------------------------------------------------------------------------- /test/syms/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "archive/tar" 5 | "bufio" 6 | "bytes" 7 | "fmt" 8 | "io" 9 | "maps" 10 | "net/http" 11 | "os" 12 | "path/filepath" 13 | "slices" 14 | "sort" 15 | "strconv" 16 | "strings" 17 | 18 | "github.com/pgaskin/kobopatch/patchlib" 19 | "github.com/xi2/xz" 20 | ) 21 | 22 | var githubActions, _ = strconv.ParseBool(os.Getenv("GITHUB_ACTIONS")) // for https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands 23 | 24 | func main() { 25 | sc, err := FindSymChecks(".") 26 | if err != nil { 27 | fmt.Fprintf(os.Stderr, "[FTL] find symbol checks: %v\n", err) 28 | os.Exit(1) 29 | return 30 | } 31 | 32 | versions := []string{ 33 | "4.6.9960", "4.6.9995", "4.7.10075", "4.7.10364", "4.7.10413", 34 | "4.8.10956", "4.8.11073", "4.8.11090", "4.9.11311", "4.9.11314", 35 | "4.10.11591", "4.10.11655", "4.11.11911", "4.11.11976", "4.11.11980", 36 | "4.11.11982", "4.11.12019", "4.12.12111", "4.13.12638", "4.14.12777", 37 | "4.15.12920", "4.16.13162", "4.17.13651", "4.17.13694", "4.18.13737", 38 | "4.19.14123", "4.20.14601", "4.20.14617", "4.20.14622", "4.21.15015", 39 | "4.22.15190", "4.22.15268", "4.23.15505", "4.24.15672", "4.24.15676", 40 | "4.25.15875", "4.26.16704", "4.28.17623", "4.28.17820", "4.28.17826", 41 | "4.28.17925", "4.28.18220", "4.29.18730", "4.30.18838", "4.31.19086", 42 | "4.32.19501", "4.33.19608", "4.33.19611", "4.33.19759", "4.34.20097", 43 | "4.35.20400", "4.36.21095", "4.37.21533", "4.37.21582", "4.37.21586", 44 | "4.38.21908", "4.39.22801", "4.39.22861", "4.38.23038", "4.39.23027", 45 | "4.40.23081", "4.41.23145", "4.38.23171", "4.42.23296", "4.38.23429", 46 | "4.43.23418", "4.38.23552", "4.44.23552", "4.38.23555", 47 | } 48 | 49 | checks := map[string]map[string][]SymCheck{} 50 | for _, c := range sc { 51 | var sm, em int 52 | for _, version := range versions { 53 | if c.StartVersion == "*" || strings.HasPrefix(version+".", c.StartVersion+".") { 54 | sm++ 55 | } 56 | if c.EndVersion == "*" || strings.HasPrefix(version+".", c.EndVersion+".") { 57 | em++ 58 | } 59 | if versioncmp(c.StartVersion, version) <= 0 && versioncmp(version, c.EndVersion) <= 0 { 60 | if _, ok := checks[version]; !ok { 61 | checks[version] = map[string][]SymCheck{} 62 | } 63 | checks[version][c.Library] = append(checks[version][c.Library], c) 64 | } 65 | } 66 | if sm == 0 { 67 | fmt.Printf("[WRN] %s: no exact match for the base version in specifier %#v\n", c.File, c.StartVersion) 68 | } 69 | if em == 0 { 70 | fmt.Printf("[WRN] %s: no exact match for the base version in specifier %#v\n", c.File, c.EndVersion) 71 | } 72 | } 73 | 74 | checkVersions := slices.SortedFunc(maps.Keys(checks), versioncmp) 75 | fmt.Printf("[INF] sorted versions: %s\n", checkVersions) 76 | 77 | var errs []error 78 | gherrs := map[string][]string{} 79 | for _, version := range checkVersions { 80 | if githubActions { 81 | fmt.Printf("::group::%s\n", version) 82 | } 83 | var checkLibs []string 84 | for lib := range checks[version] { 85 | checkLibs = append(checkLibs, lib) 86 | } 87 | sort.Strings(checkLibs) 88 | 89 | for _, lib := range checkLibs { 90 | fmt.Printf("[INF] checking %s@%s\n", lib, version) 91 | 92 | pt, err := GetPatcher(version, lib) 93 | if err != nil { 94 | fmt.Fprintf(os.Stderr, "[FTL] get patcher: %v\n", err) 95 | os.Exit(1) 96 | return 97 | } else if pt == nil { 98 | fmt.Printf("[WRN] no data available, skipping\n") 99 | continue 100 | } 101 | 102 | _, err = pt.ExtractDynsyms(true) 103 | if err != nil { 104 | fmt.Fprintf(os.Stderr, "[FTL] extract symbols: %v\n", err) 105 | os.Exit(1) 106 | return 107 | } 108 | 109 | for _, check := range checks[version][lib] { 110 | fmt.Printf("[INF] %s:\n checking for one of %+s\n", check.File, check.Symbols) 111 | var f bool 112 | for _, sym := range check.Symbols { 113 | off, err := pt.ResolveSym(sym) 114 | if err != nil { 115 | fmt.Printf(" %s not found\n", sym) 116 | } else { 117 | fmt.Printf(" %s found at %#x\n", sym, off) 118 | f = true 119 | } 120 | } 121 | if !f { 122 | err := fmt.Errorf("%s: one of %+s not found in %s@%s", check.File, check.Symbols, lib, version) 123 | fmt.Printf("[ERR] %v\n", err) 124 | errs = append(errs, err) 125 | 126 | spl := strings.Split(check.File, ":") 127 | gherrf := fmt.Sprintf("file=%s,line=%s,col=%s", spl[0], spl[1], spl[2]) 128 | gherrs[gherrf] = append(gherrs[gherrf], fmt.Sprintf("one of symbols %+s not found in %s@%s", check.Symbols, lib, version)) 129 | } 130 | } 131 | } 132 | 133 | if githubActions { 134 | fmt.Printf("::endgroup::\n") 135 | } 136 | } 137 | if len(errs) == 0 { 138 | os.Exit(0) 139 | } 140 | 141 | fmt.Printf("[FTL] check failed\n") 142 | for _, err := range errs { 143 | fmt.Printf(" %v\n", err) 144 | } 145 | if githubActions { 146 | var ghfs []string 147 | for ghf := range gherrs { 148 | ghfs = append(ghfs, ghf) 149 | } 150 | sort.Strings(ghfs) 151 | for _, ghf := range ghfs { 152 | fmt.Printf("::error %s::%s\n", ghf, strings.Join(gherrs[ghf], "%0A")) 153 | } 154 | } 155 | os.Exit(1) 156 | } 157 | 158 | func GetPatcher(version, lib string) (*patchlib.Patcher, error) { 159 | resp, err := http.Get("https://github.com/pgaskin/kobopatch-testdata/raw/v1/" + version + ".tar.xz") 160 | if err != nil { 161 | return nil, fmt.Errorf("get kobopatch testdata for %#v: %w", version, err) 162 | } 163 | defer resp.Body.Close() 164 | 165 | if resp.StatusCode == http.StatusNotFound { 166 | return nil, nil 167 | } else if resp.StatusCode != http.StatusOK { 168 | return nil, fmt.Errorf("get kobopatch testdata for %#v: response status %s", version, resp.Status) 169 | } 170 | 171 | zr, err := xz.NewReader(resp.Body, 0) 172 | if err != nil { 173 | return nil, fmt.Errorf("read kobopatch testdata: %w", err) 174 | } 175 | 176 | tr := tar.NewReader(zr) 177 | for { 178 | th, err := tr.Next() 179 | if err != nil { 180 | if err == io.EOF { 181 | return nil, fmt.Errorf("read kobopatch testdata: file %#v not found", lib) 182 | } 183 | return nil, fmt.Errorf("read kobopatch testdata: %w", err) 184 | } 185 | if filepath.Clean(th.Name) == filepath.Clean(lib) { 186 | break 187 | } 188 | } 189 | 190 | buf, err := io.ReadAll(tr) 191 | if err != nil { 192 | return nil, fmt.Errorf("read kobopatch testdata: %w", err) 193 | } 194 | return patchlib.NewPatcher(buf), nil 195 | } 196 | 197 | type SymCheck struct { 198 | File string 199 | Library string 200 | StartVersion string 201 | EndVersion string 202 | Symbols []string // or 203 | } 204 | 205 | func FindSymChecks(dir string) ([]SymCheck, error) { 206 | var checks []SymCheck 207 | if err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { 208 | var m bool 209 | for _, ext := range []string{".c", ".cc", ".cpp", ".h"} { 210 | if filepath.Ext(path) == ext { 211 | m = true 212 | break 213 | } 214 | } 215 | if !m { 216 | return nil 217 | } 218 | 219 | f, err := os.OpenFile(path, os.O_RDONLY, 0) 220 | if err != nil { 221 | return fmt.Errorf("open %#v: %w", path, err) 222 | } 223 | defer f.Close() 224 | 225 | sc := bufio.NewScanner(f) 226 | var line int 227 | for sc.Scan() { 228 | line++ 229 | col := bytes.Index(sc.Bytes(), []byte("//libnickel")) 230 | if col == -1 { 231 | continue 232 | } 233 | 234 | args := strings.Fields(string(bytes.TrimSpace(sc.Bytes()[col+len("//libnickel"):]))) 235 | if len(args) < 3 || args[0] == "*" { 236 | return fmt.Errorf("parse %#v: line %d, col %d: expected comment to be in the format '//libnickel ...'", path, line, col+1) 237 | } 238 | 239 | checks = append(checks, SymCheck{ 240 | File: fmt.Sprintf("%s:%d:%d", path, line, col+1), 241 | Library: "libnickel.so.1.0.0", 242 | StartVersion: args[0], 243 | EndVersion: args[1], 244 | Symbols: args[2:], 245 | }) 246 | } 247 | if err := sc.Err(); err != nil { 248 | return fmt.Errorf("read %#v: %w", path, err) 249 | } 250 | 251 | return nil 252 | }); err != nil { 253 | return nil, err 254 | } 255 | return checks, nil 256 | } 257 | 258 | func versioncmp(a, b string) int { 259 | if a == "*" || b == "*" { 260 | return 0 261 | } 262 | aspl, bspl := splint(a), splint(b) 263 | if false { // I think it might be less confusing to just explicitly list both if required, as they are branches and not all changes are in each 264 | if len(aspl) == 3 && len(bspl) == 3 && aspl[0] == 4 && bspl[0] == 4 && aspl[1] >= 38 && bspl[1] >= 38 { 265 | // if 4.38/4.39+ branching, sort by only the build number 266 | switch { 267 | case aspl[2] < bspl[2]: 268 | return -1 269 | case aspl[2] > bspl[2]: 270 | return 1 271 | case aspl[2] == bspl[2] && aspl[1] == bspl[1]: // fall back to normal compare if not entirely equal 272 | return 0 273 | } 274 | } 275 | } 276 | mlen := len(aspl) 277 | if len(bspl) > mlen { 278 | mlen = len(bspl) 279 | } 280 | for i := 0; i < mlen; i++ { 281 | switch { 282 | case i == len(bspl): 283 | return 1 284 | case i == len(aspl): 285 | return -1 286 | case aspl[i] > bspl[i]: 287 | return 1 288 | case bspl[i] > aspl[i]: 289 | return -1 290 | } 291 | } 292 | return 0 293 | } 294 | 295 | func splint(str string) []int64 { 296 | spl := strings.Split(str, ".") 297 | ints := make([]int64, len(spl)) 298 | for i, p := range spl { 299 | ints[i], _ = strconv.ParseInt(p, 10, 64) 300 | } 301 | return ints 302 | } 303 | -------------------------------------------------------------------------------- /src/kfmon.c: -------------------------------------------------------------------------------- 1 | #define _GNU_SOURCE 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "kfmon.h" 14 | #include "kfmon_helpers.h" 15 | #include "util.h" 16 | 17 | // Free all resources allocated by a list and its nodes 18 | inline void kfmon_teardown_list(kfmon_watch_list_t *list) { 19 | kfmon_watch_node_t *node = list->head; 20 | while (node) { 21 | kfmon_watch_node_t *p = node->next; 22 | free(node->watch.filename); 23 | free(node->watch.label); 24 | free(node); 25 | node = p; 26 | } 27 | // Don't leave dangling pointers 28 | list->head = NULL; 29 | list->tail = NULL; 30 | } 31 | 32 | // Allocate a single new node to the list 33 | inline int kfmon_grow_list(kfmon_watch_list_t *list) { 34 | kfmon_watch_node_t *prev = list->tail; 35 | kfmon_watch_node_t *node = calloc(1, sizeof(*node)); 36 | if (!node) { 37 | return KFMON_IPC_CALLOC_FAILURE; 38 | } 39 | list->count++; 40 | 41 | // Update the head if this is the first node 42 | if (!list->head) { 43 | list->head = node; 44 | } 45 | // Update the tail pointer 46 | list->tail = node; 47 | // If there was a previous node, link the two together 48 | if (prev) { 49 | prev->next = node; 50 | } 51 | 52 | return EXIT_SUCCESS; 53 | } 54 | 55 | // Handle replies from the IPC socket 56 | static int handle_reply(int data_fd, void *data __attribute__((unused))) { 57 | // Eh, recycle PIPE_BUF, it should be more than enough for our needs. 58 | char buf[PIPE_BUF] = { 0 }; 59 | 60 | // We don't actually know the size of the reply, so, best effort here. 61 | ssize_t len = xread(data_fd, buf, sizeof(buf)); 62 | if (len < 0) { 63 | // Only actual failures are left, xread handles the rest 64 | return KFMON_IPC_REPLY_READ_FAILURE; 65 | } 66 | 67 | // If there's actually nothing to read (EoF), abort. 68 | if (len == 0) { 69 | return KFMON_IPC_ENODATA; 70 | } 71 | 72 | // Check the reply for failures 73 | if (!strncmp(buf, "ERR_INVALID_ID", 14)) { 74 | return KFMON_IPC_ERR_INVALID_ID; 75 | } else if (!strncmp(buf, "WARN_ALREADY_RUNNING", 20)) { 76 | return KFMON_IPC_WARN_ALREADY_RUNNING; 77 | } else if (!strncmp(buf, "WARN_SPAWN_BLOCKED", 18)) { 78 | return KFMON_IPC_WARN_SPAWN_BLOCKED; 79 | } else if (!strncmp(buf, "WARN_SPAWN_INHIBITED", 20)) { 80 | return KFMON_IPC_WARN_SPAWN_INHIBITED; 81 | } else if (!strncmp(buf, "ERR_REALLY_MALFORMED_CMD", 24)) { 82 | return KFMON_IPC_ERR_REALLY_MALFORMED_CMD; 83 | } else if (!strncmp(buf, "ERR_MALFORMED_CMD", 17)) { 84 | return KFMON_IPC_ERR_MALFORMED_CMD; 85 | } else if (!strncmp(buf, "ERR_INVALID_CMD", 15)) { 86 | return KFMON_IPC_ERR_INVALID_CMD; 87 | } else if (!strncmp(buf, "OK", 2)) { 88 | return EXIT_SUCCESS; 89 | } else { 90 | return KFMON_IPC_UNKNOWN_REPLY; 91 | } 92 | 93 | // We're not done until we've got a reply we're satisfied with... 94 | return KFMON_IPC_EAGAIN; 95 | } 96 | 97 | // Handle replies from a 'list' command 98 | static int handle_list_reply(int data_fd, void *data) { 99 | // Can't do it on the stack because of strsep 100 | char *buf = NULL; 101 | buf = calloc(PIPE_BUF, sizeof(*buf)); 102 | if (!buf) { 103 | return KFMON_IPC_CALLOC_FAILURE; 104 | } 105 | 106 | // Until proven otherwise... 107 | int status = EXIT_SUCCESS; 108 | 109 | // We don't actually know the size of the reply, so, best effort here. 110 | ssize_t len = xread(data_fd, buf, PIPE_BUF); 111 | if (len < 0) { 112 | // Only actual failures are left, xread handles the rest 113 | status = KFMON_IPC_REPLY_READ_FAILURE; 114 | goto cleanup; 115 | } 116 | 117 | // If there's actually nothing to read (EoF), abort. 118 | if (len == 0) { 119 | status = KFMON_IPC_ENODATA; 120 | goto cleanup; 121 | } 122 | 123 | // The only valid reply for list is... a list ;). 124 | if (!strncmp(buf, "ERR_INVALID_CMD", 15)) { 125 | status = KFMON_IPC_ERR_INVALID_CMD; 126 | goto cleanup; 127 | } else if ((!strncmp(buf, "WARN_", 5)) || 128 | (!strncmp(buf, "ERR_", 4)) || 129 | (!strncmp(buf, "OK", 2))) { 130 | status = KFMON_IPC_UNKNOWN_REPLY; 131 | goto cleanup; 132 | } 133 | 134 | // NOTE: Replies may be split across multiple reads (and as such, multiple handle_list_reply calls). 135 | // So the only way we can be sure that we're done (short of timing out after a while of no POLLIN revents, 136 | // which would be stupid), is to check that the final byte we've just read is a NUL, 137 | // as that's how KFMon terminates a list. 138 | bool eot = false; 139 | // NOTE: The parsing code later does its own take on this to detect the final line, 140 | // but this one should be authoritative, as strsep modifies the data. 141 | if (buf[len - 1] == '\0') { 142 | eot = true; 143 | } 144 | 145 | // Keep some minimal debug logging around, just in case... 146 | NM_LOG("Got a %zd bytes reply from KFMon (%s an EoT marker)", len, eot ? "with" : "*without*"); 147 | // Now that we're sure we didn't get a wonky reply from an unrelated command, parse the list 148 | // NOTE: Format is: 149 | // id:filename:label or id:filename for watches without a label 150 | // We don't care about id, as it potentially won't be stable across the full powercycle, 151 | // filename is what we pass verbatim to a kfmon action 152 | // label is our action's lbl (use filename if NULL) 153 | char *p = buf; 154 | char *line = NULL; 155 | // Break the reply line by line 156 | while ((line = strsep(&p, "\n")) != NULL) { 157 | // Then parse each line... 158 | // If it's the final line, its only content is a single NUL 159 | if (*line == '\0') { 160 | // NOTE: This might also simply be the end of a single-line read, 161 | // in which case the NUL is thanks to calloc... 162 | break; 163 | } 164 | NM_LOG("Parsing reply line: `%s`", line); 165 | // NOTE: Simple syslog logging for now 166 | char *watch_idx = strsep(&line, ":"); 167 | if (!watch_idx) { 168 | status = KFMON_IPC_LIST_PARSE_FAILURE; 169 | goto cleanup; 170 | } 171 | char *filename = strsep(&line, ":"); 172 | if (!filename) { 173 | status = KFMON_IPC_LIST_PARSE_FAILURE; 174 | goto cleanup; 175 | } 176 | // Final separator is optional, if it's not there, there's no label, use the filename instead. 177 | char *label = strsep(&line, ":"); 178 | 179 | // Store that at the tail of the list 180 | kfmon_watch_list_t *list = (kfmon_watch_list_t*) data; 181 | // Make room for a new node 182 | if (kfmon_grow_list(list) != EXIT_SUCCESS) { 183 | status = KFMON_IPC_CALLOC_FAILURE; 184 | goto cleanup; 185 | } 186 | // Use it 187 | kfmon_watch_node_t *node = list->tail; 188 | 189 | node->watch.idx = (uint8_t) strtoul(watch_idx, NULL, 10); 190 | node->watch.filename = strdup(filename); 191 | node->watch.label = label ? strdup(label) : strdup(filename); 192 | } 193 | 194 | // Are we really done? 195 | status = eot ? EXIT_SUCCESS : KFMON_IPC_EAGAIN; 196 | 197 | cleanup: 198 | free(buf); 199 | return status; 200 | } 201 | 202 | // Connect to KFMon's IPC socket. Returns error code, store data fd by ref. 203 | static int connect_to_kfmon_socket(int *restrict data_fd) { 204 | // Setup the local socket 205 | if ((*data_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0)) == -1) { 206 | return KFMON_IPC_SOCKET_FAILURE; 207 | } 208 | 209 | struct sockaddr_un sock_name = { 0 }; 210 | sock_name.sun_family = AF_UNIX; 211 | strncpy(sock_name.sun_path, KFMON_IPC_SOCKET, sizeof(sock_name.sun_path) - 1); 212 | 213 | // Connect to IPC socket, retrying safely on EINTR (c.f., http://www.madore.org/~david/computers/connect-intr.html) 214 | while (connect(*data_fd, (const struct sockaddr*) &sock_name, sizeof(sock_name)) == -1 && errno != EISCONN) { 215 | if (errno != EINTR) { 216 | return KFMON_IPC_CONNECT_FAILURE; 217 | } 218 | } 219 | 220 | // Wheee! 221 | return EXIT_SUCCESS; 222 | } 223 | 224 | // Send a packet to KFMon over the wire (payload *MUST* be NUL-terminated to avoid truncation, and len *MUST* include that NUL). 225 | static int send_packet(int data_fd, const char *restrict payload, size_t len) { 226 | // Send it (w/ a NUL) 227 | if (send_in_full(data_fd, payload, len) < 0) { 228 | // Only actual failures are left 229 | if (errno == EPIPE) { 230 | return KFMON_IPC_EPIPE; 231 | } else { 232 | return KFMON_IPC_SEND_FAILURE; 233 | } 234 | } 235 | 236 | // Wheee! 237 | return EXIT_SUCCESS; 238 | } 239 | 240 | // Send the requested IPC command:arg pair (or command alone if arg is NULL) 241 | static int send_ipc_command(int data_fd, const char *restrict ipc_cmd, const char *restrict ipc_arg) { 242 | char buf[256] = { 0 }; 243 | int packet_len = 0; 244 | // Somme commands don't require an arg 245 | if (ipc_arg) { 246 | packet_len = snprintf(buf, sizeof(buf), "%s:%s", ipc_cmd, ipc_arg); 247 | } else { 248 | packet_len = snprintf(buf, sizeof(buf), "%s", ipc_cmd); 249 | } 250 | // Send it (w/ a NUL) 251 | return send_packet(data_fd, buf, (size_t) (packet_len + 1)); 252 | } 253 | 254 | // Poll the IPC socket for potentially *multiple* replies to a single command, timeout after attempts * timeout (ms) 255 | static int wait_for_replies(int data_fd, int timeout, size_t attempts, ipc_handler_t reply_handler, void **data) { 256 | int status = EXIT_SUCCESS; 257 | 258 | struct pollfd pfd = { 0 }; 259 | // Data socket 260 | pfd.fd = data_fd; 261 | pfd.events = POLLIN; 262 | 263 | // Here goes... We'll wait for windows of ms 264 | size_t retry = 0U; 265 | while (1) { 266 | int poll_num = poll(&pfd, 1, timeout); 267 | if (poll_num == -1) { 268 | if (errno == EINTR) { 269 | continue; 270 | } 271 | return KFMON_IPC_POLL_FAILURE; 272 | } 273 | 274 | if (poll_num > 0) { 275 | if (pfd.revents & POLLIN) { 276 | // There was a reply from the socket 277 | int reply = reply_handler(data_fd, data); 278 | if (reply != EXIT_SUCCESS) { 279 | // If the remote closed the connection, we get POLLIN|POLLHUP w/ EoF ;). 280 | if (pfd.revents & POLLHUP) { 281 | // Flag that as an error 282 | status = KFMON_IPC_EPIPE; 283 | } else { 284 | if (reply == KFMON_IPC_EAGAIN) { 285 | // We're expecting more stuff to read, keep going. 286 | continue; 287 | } else { 288 | // Something went wrong when handling the reply, pass the error as-is 289 | status = reply; 290 | } 291 | } 292 | // We're obviously done if something went wrong. 293 | break; 294 | } else { 295 | // We break on success, too, as we only need to send a single command. 296 | status = EXIT_SUCCESS; 297 | break; 298 | } 299 | } 300 | 301 | // Remote closed the connection 302 | if (pfd.revents & POLLHUP) { 303 | // Flag that as an error 304 | status = KFMON_IPC_EPIPE; 305 | break; 306 | } 307 | } 308 | 309 | if (poll_num == 0) { 310 | // Timed out, increase the retry counter 311 | retry++; 312 | } 313 | 314 | // Drop the axe after the final attempt 315 | if (retry >= attempts) { 316 | status = KFMON_IPC_ETIMEDOUT; 317 | break; 318 | } 319 | } 320 | 321 | return status; 322 | } 323 | 324 | // Handle a simple KFMon IPC request 325 | int nm_kfmon_simple_request(const char *restrict ipc_cmd, const char *restrict ipc_arg) { 326 | // Assume everything's peachy until shit happens... 327 | int status = EXIT_SUCCESS; 328 | 329 | int data_fd = -1; 330 | // Attempt to connect to KFMon... 331 | // As long as KFMon is up, has very little chance to fail, even if the connection backlog is full. 332 | status = connect_to_kfmon_socket(&data_fd); 333 | // If it failed, return early 334 | if (status != EXIT_SUCCESS) { 335 | return status; 336 | } 337 | 338 | // Attempt to send the specified command in full over the wire 339 | status = send_ipc_command(data_fd, ipc_cmd, ipc_arg); 340 | // If it failed, return early, after closing the socket 341 | if (status != EXIT_SUCCESS) { 342 | close(data_fd); 343 | return status; 344 | } 345 | 346 | // We'll be polling the socket for a reply, this'll make things neater, and allows us to abort on timeout, 347 | // in the unlikely event there's already an IPC session being handled by KFMon, 348 | // in which case the reply would be delayed by an undeterminate amount of time (i.e., until KFMon gets to it). 349 | // Here, we'll want to timeout after 2s 350 | ipc_handler_t handler = &handle_reply; 351 | status = wait_for_replies(data_fd, 500, 4, handler, NULL); 352 | // NOTE: We happen to be done with the connection right now. 353 | // But if we still needed it, KFMON_IPC_POLL_FAILURE would warrant an early abort w/ a forced close(). 354 | 355 | // Bye now! 356 | close(data_fd); 357 | return status; 358 | } 359 | 360 | // Handle a list request for the KFMon generator 361 | int nm_kfmon_list_request(const char *restrict ipc_cmd, kfmon_watch_list_t *list) { 362 | // Assume everything's peachy until shit happens... 363 | int status = EXIT_SUCCESS; 364 | 365 | int data_fd = -1; 366 | // Attempt to connect to KFMon... 367 | // As long as KFMon is up, has very little chance to fail, even if the connection backlog is full. 368 | status = connect_to_kfmon_socket(&data_fd); 369 | // If it failed, return early 370 | if (status != EXIT_SUCCESS) { 371 | return status; 372 | } 373 | 374 | // Attempt to send the specified command in full over the wire 375 | status = send_ipc_command(data_fd, ipc_cmd, NULL); 376 | // If it failed, return early, after closing the socket 377 | if (status != EXIT_SUCCESS) { 378 | close(data_fd); 379 | return status; 380 | } 381 | 382 | // We'll be polling the socket for a reply, this'll make things neater, and allows us to abort on timeout, 383 | // in the unlikely event there's already an IPC session being handled by KFMon, 384 | // in which case the reply would be delayed by an undeterminate amount of time (i.e., until KFMon gets to it). 385 | // Here, we'll want to timeout after 2s 386 | ipc_handler_t handler = &handle_list_reply; 387 | status = wait_for_replies(data_fd, 500, 4, handler, (void *) list); 388 | // NOTE: We happen to be done with the connection right now. 389 | // But if we still needed it, KFMON_IPC_POLL_FAILURE would warrant an early abort w/ a forced close(). 390 | 391 | // Bye now! 392 | close(data_fd); 393 | return status; 394 | } 395 | 396 | // Giant ladder of fail 397 | bool nm_kfmon_error_handler(kfmon_ipc_errno_e status) { 398 | switch (status) { 399 | case KFMON_IPC_OK: 400 | return nm_err_set(NULL); 401 | // Fail w/ the right log message 402 | case KFMON_IPC_ETIMEDOUT: 403 | return nm_err_set("Timed out waiting for KFMon"); 404 | case KFMON_IPC_EPIPE: 405 | return nm_err_set("KFMon closed the connection"); 406 | case KFMON_IPC_ENODATA: 407 | return nm_err_set("No more data to read"); 408 | case KFMON_IPC_READ_FAILURE: 409 | // NOTE: Let's hope close() won't mangle errno... 410 | return nm_err_set("read: %m"); 411 | case KFMON_IPC_SEND_FAILURE: 412 | // NOTE: Let's hope close() won't mangle errno... 413 | return nm_err_set("send: %m"); 414 | case KFMON_IPC_SOCKET_FAILURE: 415 | return nm_err_set("Failed to create local KFMon IPC socket (socket: %m)"); 416 | case KFMON_IPC_CONNECT_FAILURE: 417 | return nm_err_set("KFMon IPC is down (connect: %m)"); 418 | case KFMON_IPC_POLL_FAILURE: 419 | // NOTE: Let's hope close() won't mangle errno... 420 | return nm_err_set("poll: %m"); 421 | case KFMON_IPC_CALLOC_FAILURE: 422 | return nm_err_set("calloc: %m"); 423 | case KFMON_IPC_REPLY_READ_FAILURE: 424 | // NOTE: Let's hope close() won't mangle errno... 425 | return nm_err_set("Failed to read KFMon's reply (%m)"); 426 | case KFMON_IPC_LIST_PARSE_FAILURE: 427 | return nm_err_set("Failed to parse the list of watches (no separator found)"); 428 | case KFMON_IPC_ERR_INVALID_ID: 429 | return nm_err_set("Requested to start an invalid watch index"); 430 | case KFMON_IPC_ERR_INVALID_NAME: 431 | return nm_err_set("Requested to trigger an invalid watch filename (expected the basename of the image trigger)"); 432 | case KFMON_IPC_WARN_ALREADY_RUNNING: 433 | return nm_err_set("Requested watch is already running"); 434 | case KFMON_IPC_WARN_SPAWN_BLOCKED: 435 | return nm_err_set("A spawn blocker is currently running"); 436 | case KFMON_IPC_WARN_SPAWN_INHIBITED: 437 | return nm_err_set("Spawns are currently inhibited"); 438 | case KFMON_IPC_ERR_REALLY_MALFORMED_CMD: 439 | return nm_err_set("KFMon couldn't parse our command"); 440 | case KFMON_IPC_ERR_MALFORMED_CMD: 441 | return nm_err_set("Bad command syntax"); 442 | case KFMON_IPC_ERR_INVALID_CMD: 443 | return nm_err_set("Command wasn't recognized by KFMon"); 444 | case KFMON_IPC_UNKNOWN_REPLY: 445 | return nm_err_set("We couldn't make sense of KFMon's reply"); 446 | case KFMON_IPC_EAGAIN: 447 | default: 448 | // Should never happen 449 | return nm_err_set("Something went wrong"); 450 | } 451 | } 452 | 453 | nm_action_result_t *nm_kfmon_return_handler(kfmon_ipc_errno_e status) { 454 | if (!nm_kfmon_error_handler(status)) 455 | return nm_action_result_silent(); 456 | return NULL; 457 | } 458 | -------------------------------------------------------------------------------- /res/doc: -------------------------------------------------------------------------------- 1 | # NickelMenu (libnm.so) 2 | # https://pgaskin.net/NickelMenu 3 | # 4 | # This tool injects menu items into Nickel. 5 | # 6 | # It should work on firmware 4.6+, but it has only been tested on 4.20.14622 - 7 | # 4.31.19086. It is perfectly safe to try out on any newer firmware version, as 8 | # it has a lot of error checking, and a failsafe mechanism which automatically 9 | # uninstalls it as a last resort. 10 | # 11 | # Place your configuration files in this folder. They can be named anything, and 12 | # should consist of multiple lines either starting with # for a comment, or in 13 | # one of the the following formats (spaces around fields are ignored): 14 | # 15 | # menu_item::