├── .dockerignore ├── .gitignore ├── images └── inotify-info.png ├── Dockerfile ├── .github └── workflows │ ├── release.yaml │ └── ci.yaml ├── LICENSE ├── lfqueue ├── LICENSE ├── lfqueue.h └── lfqueue.c ├── inotify-info.h ├── Makefile ├── README.md └── inotify-info.cpp /.dockerignore: -------------------------------------------------------------------------------- 1 | _release 2 | _debug 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _debug/ 2 | _release/ 3 | 4 | /inotify-info-*.tar.gz 5 | -------------------------------------------------------------------------------- /images/inotify-info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikesart/inotify-info/HEAD/images/inotify-info.png -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | 3 | RUN apk add make zig git 4 | 5 | WORKDIR inotify-info 6 | 7 | COPY . . 8 | 9 | RUN CC="zig cc -target $(uname -m)-linux-musl" \ 10 | CXX="zig c++ -target $(uname -m)-linux-musl" \ 11 | make VERBOSE=1 12 | 13 | FROM scratch 14 | COPY --from=0 /inotify-info/_release/inotify-info /inotify-info 15 | 16 | ENTRYPOINT ["/inotify-info"] 17 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: release 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Release 12 | uses: softprops/action-gh-release@v2 13 | if: startsWith(github.ref, 'refs/tags/v') 14 | with: 15 | generate_release_notes: true 16 | draft: true 17 | prerelease: true 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2021 Michael Sartain 4 | 5 | All Rights Reserved. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /lfqueue/LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2018, Taymindis Woon 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /inotify-info.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Michael Sartain 3 | * 4 | * All Rights Reserved. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | #define TO_STR(x) #x 26 | #define TO_STR_VALUE(x) TO_STR(x) 27 | 28 | #define ATTRIBUTE_PRINTF(_x, _y) __attribute__((__format__(__printf__, _x, _y))) 29 | 30 | #define GCC_DIAG_STR(s) #s 31 | #define GCC_DIAG_JOINSTR(x, y) GCC_DIAG_STR(x##y) 32 | #define GCC_DIAG_DO_PRAGMA(x) _Pragma(#x) 33 | #define GCC_DIAG_PRAGMA(x) GCC_DIAG_DO_PRAGMA(GCC diagnostic x) 34 | 35 | #define GCC_DIAG_PUSH_OFF(x) \ 36 | GCC_DIAG_PRAGMA(push) \ 37 | GCC_DIAG_PRAGMA(ignored GCC_DIAG_JOINSTR(-W, x)) 38 | #define GCC_DIAG_POP() GCC_DIAG_PRAGMA(pop) 39 | 40 | std::string string_formatv(const char* fmt, va_list ap) ATTRIBUTE_PRINTF(1, 0); 41 | std::string string_format(const char* fmt, ...) ATTRIBUTE_PRINTF(1, 2); 42 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: test 3 | on: 4 | push: 5 | pull_request: 6 | concurrency: 7 | # Cancels pending runs when a PR gets updated. 8 | group: ${{ github.head_ref || github.run_id }}-${{ github.actor }} 9 | cancel-in-progress: true 10 | jobs: 11 | 12 | lint: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - run: sudo apt-get install -y make clang-format 17 | - run: make lint 18 | - run: git diff --exit-code 19 | 20 | build-with-gcc: 21 | strategy: 22 | matrix: 23 | cfg: [debug, release] 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v4 27 | with: {fetch-depth: 0} 28 | - run: sudo apt-get install -y build-essential 29 | - run: make VERBOSE=1 CFG=${{ matrix.cfg}} -j$(nproc) 30 | - run: | 31 | _${{ matrix.cfg }}/inotify-info --version 32 | _${{ matrix.cfg }}/inotify-info 33 | 34 | build-with-zig: 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v4 38 | with: {fetch-depth: 0} 39 | - uses: actions/cache@v4 40 | with: 41 | key: zig-sdk-and-cache-${{ hashFiles('.github/workflows/ci.yaml') }} 42 | path: ~/.cache/zig 43 | - run: | 44 | wget --progress=dot:mega \ 45 | https://ziglang.org/download/0.13.0/zig-linux-$(uname -m)-0.13.0.tar.xz 46 | tar -xJf zig-linux-*.tar.xz 47 | rm zig-linux-*.xz 48 | mv zig-linux-* zig-sdk 49 | - run: | 50 | make VERBOSE=1 -j$(nproc) \ 51 | CC="zig-sdk/zig cc -target $(uname -m)-linux-musl" \ 52 | CXX="zig-sdk/zig c++ -target $(uname -m)-linux-musl" 53 | - run: | 54 | _release/inotify-info --version 55 | _release/inotify-info 56 | 57 | build-with-docker: 58 | runs-on: ubuntu-latest 59 | steps: 60 | - uses: actions/checkout@v4 61 | - run: docker build -t inotify-info . 62 | - run: docker run --rm --privileged -v /proc:/proc inotify-info 63 | -------------------------------------------------------------------------------- /lfqueue/lfqueue.h: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * BSD 2-Clause License 4 | * 5 | * Copyright (c) 2018, Taymindis Woon 6 | * All rights reserved. 7 | * 8 | * Redistribution and use in source and binary forms, with or without 9 | * modification, are permitted provided that the following conditions are met: 10 | * 11 | * * Redistributions of source code must retain the above copyright notice, this 12 | * list of conditions and the following disclaimer. 13 | * 14 | * * Redistributions in binary form must reproduce the above copyright notice, 15 | * this list of conditions and the following disclaimer in the documentation 16 | * and/or other materials provided with the distribution. 17 | * 18 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | * 29 | */ 30 | 31 | #ifndef LFQUEUE_H 32 | #define LFQUEUE_H 33 | 34 | #include 35 | #include 36 | 37 | #ifdef __cplusplus 38 | extern "C" { 39 | #endif 40 | 41 | typedef struct lfqueue_cas_node_s lfqueue_cas_node_t; 42 | typedef void* (*lfqueue_malloc_fn)(void*, size_t); 43 | typedef void (*lfqueue_free_fn)(void*, void*); 44 | 45 | #if defined __GNUC__ || defined __CYGWIN__ || defined __MINGW32__ || defined __APPLE__ 46 | #define lfq_bool_t int 47 | #else 48 | #ifdef _WIN64 49 | #define lfq_bool_t int64_t 50 | #else 51 | #define lfq_bool_t int 52 | #endif 53 | #endif 54 | 55 | typedef struct { 56 | lfqueue_cas_node_t *head, *tail, *root_free, *move_free; 57 | volatile size_t size; 58 | volatile lfq_bool_t in_free_mode; 59 | lfqueue_malloc_fn _malloc; 60 | lfqueue_free_fn _free; 61 | void* pl; 62 | } lfqueue_t; 63 | 64 | extern int lfqueue_init(lfqueue_t* lfqueue); 65 | extern int lfqueue_init_mf(lfqueue_t* lfqueue, void* pl, lfqueue_malloc_fn lfqueue_malloc, lfqueue_free_fn lfqueue_free); 66 | extern int lfqueue_enq(lfqueue_t* lfqueue, void* value); 67 | extern void* lfqueue_deq(lfqueue_t* lfqueue); 68 | extern void* lfqueue_single_deq(lfqueue_t* lfqueue); 69 | 70 | /** loop until value been dequeue, it sleeps 1ms if not found to reduce cpu high usage **/ 71 | extern void* lfqueue_deq_must(lfqueue_t* lfqueue); 72 | extern void* lfqueue_single_deq_must(lfqueue_t* lfqueue); 73 | 74 | extern void lfqueue_destroy(lfqueue_t* lfqueue); 75 | extern size_t lfqueue_size(lfqueue_t* lfqueue); 76 | extern void lfqueue_sleep(unsigned int milisec); 77 | 78 | #ifdef __cplusplus 79 | } 80 | #endif 81 | 82 | #endif 83 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # $@: name of the target file (one before colon) 2 | # $<: name of first prerequisite file (first one after colon) 3 | # $^: names of all prerequisite files (space separated) 4 | # $*: stem (bit which matches the % wildcard in rule definition) 5 | # 6 | # VAR = val: Normal setting - values within are recursively expand when var used. 7 | # VAR := val: Setting of var with simple expansion of values inside - values are expanded at decl time. 8 | # VAR ?= val: Set var only if it doesn't have a value. 9 | # VAR += val: Append val to existing value (or set if var didn't exist). 10 | 11 | # To use static analyzer: 12 | # http://clang-analyzer.llvm.org/scan-build.html 13 | # Ie: 14 | # scan-build -k -V --use-analyzer ~/bin/clang make 15 | 16 | NAME = inotify-info 17 | 18 | CFG ?= release 19 | ifeq ($(CFG), debug) 20 | ASAN ?= 1 21 | endif 22 | 23 | PREFIX ?= /usr/local 24 | BINDIR := $(PREFIX)/bin 25 | 26 | LD = $(CC) 27 | RM = rm -f 28 | MKDIR = mkdir -p 29 | VERBOSE ?= 0 30 | 31 | INOTIFYINFO_VERSION ?= $(shell git describe --tags --dirty 2>/dev/null || echo unknown) 32 | 33 | COMPILER = $(shell $(CC) -v 2>&1 | grep -q "clang version" && echo clang || echo gcc) 34 | 35 | WARNINGS = -Wall -Wextra -Wpedantic -Wmissing-include-dirs -Wformat=2 -Wshadow 36 | ifneq ($(COMPILER),clang) 37 | # https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html 38 | WARNINGS += -Wsuggest-attribute=format -Wall 39 | endif 40 | 41 | DEFINES = -D_LARGEFILE64_SOURCE=1 -D_FILE_OFFSET_BITS=64 42 | DEFINES += -DINOTIFYINFO_VERSION=\"$(INOTIFYINFO_VERSION)\" 43 | 44 | CFLAGS += $(WARNINGS) $(DEFINES) 45 | CFLAGS += -std=gnu99 -fno-exceptions 46 | 47 | CXXFLAGS += $(WARNINGS) $(DEFINES) 48 | CXXFLAGS += -std=c++11 -fno-exceptions -fno-rtti -Woverloaded-virtual 49 | 50 | LIBS = -Wl,--no-as-needed -lm -ldl -lpthread -lstdc++ 51 | 52 | CFILES = \ 53 | inotify-info.cpp \ 54 | lfqueue/lfqueue.c 55 | 56 | # Useful GCC address sanitizer checks not enabled by default 57 | # https://kristerw.blogspot.com/2018/06/useful-gcc-address-sanitizer-checks-not.html 58 | 59 | ifeq ($(ASAN), 1) 60 | # https://gcc.gnu.org/gcc-5/changes.html 61 | # -fsanitize=float-cast-overflow: check that the result of floating-point type to integer conversions do not overflow; 62 | # -fsanitize=vptr: enable checking of C++ member function calls, member accesses and some conversions between pointers to base and derived classes, detect if the referenced object does not have the correct dynamic type. 63 | ASAN_FLAGS = -fno-omit-frame-pointer -fno-optimize-sibling-calls 64 | ASAN_FLAGS += -fsanitize=address # fast memory error detector (heap, stack, global buffer overflow, and use-after free) 65 | ASAN_FLAGS += -fsanitize=leak # detect leaks 66 | ASAN_FLAGS += -fsanitize=undefined # fast undefined behavior detector 67 | ASAN_FLAGS += -fsanitize=float-divide-by-zero # detect floating-point division by zero; 68 | ASAN_FLAGS += -fsanitize=bounds # enable instrumentation of array bounds and detect out-of-bounds accesses; 69 | ASAN_FLAGS += -fsanitize=object-size # enable object size checking, detect various out-of-bounds accesses. 70 | ASAN_FLAGS += -fsanitize=alignment # enable alignment checking, detect various misaligned objects; 71 | CFLAGS += $(ASAN_FLAGS) 72 | CXXFLAGS += $(ASAN_FLAGS) 73 | LDFLAGS += $(ASAN_FLAGS) 74 | endif 75 | 76 | ifeq ($(CFG), debug) 77 | ODIR=_debug 78 | CFLAGS += -O0 -DDEBUG 79 | CFLAGS += -D_GLIBCXX_DEBUG -D_GLIBCXX_DEBUG_PEDANTIC -D_GLIBCXX_SANITIZE_VECTOR -D_LIBCPP_DEBUG=1 -D_LIBCPP_ENABLE_DEBUG_MODE=1 80 | else 81 | ODIR=_release 82 | CFLAGS += -O2 -DNDEBUG 83 | endif 84 | 85 | PROJ = $(ODIR)/$(NAME) 86 | 87 | ifeq ($(VERBOSE), 1) 88 | VERBOSE_PREFIX= 89 | else 90 | VERBOSE_PREFIX=@ 91 | endif 92 | 93 | C_OBJS = ${CFILES:%.c=${ODIR}/%.o} 94 | OBJS = ${C_OBJS:%.cpp=${ODIR}/%.o} 95 | 96 | all: $(PROJ) 97 | 98 | $(ODIR)/$(NAME): $(OBJS) 99 | @echo "Linking $@..."; 100 | $(VERBOSE_PREFIX)$(LD) $(LDFLAGS) $^ $(LIBS) -o $@ 101 | 102 | -include $(OBJS:.o=.d) 103 | 104 | $(ODIR)/%.o: %.c Makefile 105 | $(VERBOSE_PREFIX)echo "---- $< ----"; 106 | @$(MKDIR) $(dir $@) 107 | $(VERBOSE_PREFIX)$(CC) -MMD -MP $(CFLAGS) -o $@ -c $< 108 | 109 | $(ODIR)/%.o: %.cpp Makefile 110 | $(VERBOSE_PREFIX)echo "---- $< ----"; 111 | @$(MKDIR) $(dir $@) 112 | $(VERBOSE_PREFIX)$(CXX) -MMD -MP $(CXXFLAGS) -o $@ -c $< 113 | 114 | .PHONY: lint 115 | lint: 116 | find . -name '*.h' -o -name '*.c' -o -name '*.cpp' | xargs clang-format -i --style=webkit 117 | 118 | .PHONY: clean 119 | 120 | clean: 121 | @echo Cleaning... 122 | $(VERBOSE_PREFIX)$(RM) $(PROJ) 123 | $(VERBOSE_PREFIX)$(RM) $(OBJS) 124 | $(VERBOSE_PREFIX)$(RM) $(OBJS:.o=.d) 125 | $(VERBOSE_PREFIX)$(RM) $(OBJS:.o=.dwo) 126 | 127 | .PHONY: install 128 | 129 | install: all 130 | install -D $(PROJ) $(BINDIR)/$(NAME) 131 | 132 | .PHONY: uninstall 133 | 134 | uninstall: 135 | $(RM) $(BINDIR)/$(NAME) 136 | 137 | define RELEASE_RULES 138 | inotify-info-$(TAG).tar.gz: 139 | git archive --prefix=inotify-info-$(TAG)/ v$(TAG) | gzip -n > $$@ 140 | endef 141 | $(foreach TAG,$(shell git tag 2>/dev/null | sed -n '/^v/ s/^v//p'),$(eval $(RELEASE_RULES))) 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # inotify-info 2 | > Easily track down the number of inotify watches, instances, and which files are being watched. 3 | 4 | The Linux inotify system has a few issues [1][problem1][2][problem2] and it can be difficult to debug when you for instance run out of watches. Using this app should hopefully aid you in tracking down how many inotify watches, instances, and what files are being watched. 5 | 6 | ## Screenshot 7 | ![Alt text](images/inotify-info.png?raw=true "inotify-info") 8 | 9 | ## Build 10 | ``` 11 | $ make 12 | Building _release/inotify-info... 13 | ---- inotify-info.cpp ---- 14 | ---- lfqueue/lfqueue.c ---- 15 | Linking _release/inotify-info... 16 | ``` 17 | ``` 18 | $ CFG=debug make 19 | Building _debug/inotify-info... 20 | ---- inotify-info.cpp ---- 21 | ---- lfqueue/lfqueue.c ---- 22 | Linking _debug/inotify-info... 23 | ``` 24 | 25 | ## Install 26 | 27 | The resulting executable will be at `_release/inotify-info`. You are free to 28 | copy the resulting executable to any suitable location in your `$PATH` or run 29 | `make install` to install to `/usr/local/bin`. 30 | 31 | ## Run (Prints Summary) 32 | ``` 33 | $ _release/inotify-info 34 | ------------------------------------------------------------------------------ 35 | INotify Limits: 36 | max_queued_events: 16384 37 | max_user_instances: 128 38 | max_user_watches: 65536 39 | ------------------------------------------------------------------------------ 40 | Pid App Watches Instances 41 | 2632 systemd 23 3 42 | 2653 pulseaudio 2 2 43 | 2656 dbus-daemon 2 1 44 | 2987 dbus-daemon 1 1 45 | 3056 xfsettingsd 56 1 46 | 3068 xfdesktop 10 1 47 | 3072 wrapper-2.0 6 1 48 | 3091 xfce4-clipman 1 1 49 | 3099 xiccd 1 1 50 | 3343 xfce4-terminal 1 1 51 | 3997 xfce4-appfinder 11 1 52 | 4048 xdg-desktop-portal 1 1 53 | 4086 xdg-desktop-portal-gtk 56 1 54 | 205668 vivaldi-bin 8 1 55 | 205705 vivaldi-bin 2 1 56 | ------------------------------------------------------------------------------ 57 | Total inotify Watches: 181 58 | Total inotify Instances: 18 59 | ------------------------------------------------------------------------------ 60 | ``` 61 | 62 | ## Run with Appname Filter 63 | ``` 64 | $ _release/inotify-info xfce4 65 | ------------------------------------------------------------------------------ 66 | INotify Limits: 67 | max_queued_events: 16384 68 | max_user_instances: 128 69 | max_user_watches: 65536 70 | ------------------------------------------------------------------------------ 71 | Pid App Watches Instances 72 | 2632 systemd 23 3 73 | 2653 pulseaudio 2 2 74 | 2656 dbus-daemon 2 1 75 | 2987 dbus-daemon 1 1 76 | 3056 xfsettingsd 56 1 77 | 3068 xfdesktop 10 1 78 | 3072 wrapper-2.0 6 1 79 | 3091 xfce4-clipman 1 1 80 | 94111050 [10304h] 81 | 3099 xiccd 1 1 82 | 3343 xfce4-terminal 1 1 83 | 71048655 [10304h] 84 | 3997 xfce4-appfinder 11 1 85 | 94111468 [10304h] 15339430 [10304h] 14554799 [10304h] 70254617 [10304h] 70254684 [10304h] 16786993 [10304h] 14551253 [10304h] 14550430 [10304h] 70254647 [10304h] 70254646 [10304h] 86 | 92275589 [10304h] 87 | 4048 xdg-desktop-portal 1 1 88 | 4086 xdg-desktop-portal-gtk 56 1 89 | 205668 vivaldi-bin 8 1 90 | 205705 vivaldi-bin 2 1 91 | ------------------------------------------------------------------------------ 92 | Total inotify Watches: 181 93 | Total inotify Instances: 18 94 | ------------------------------------------------------------------------------ 95 | 96 | Searching '/' for listed inodes... (8 threads) 97 | 14550430 [10304h] /usr/share/applications/ 98 | 14551253 [10304h] /usr/local/share/ 99 | 14554799 [10304h] /usr/share/xfce4/ 100 | 15339430 [10304h] /usr/share/desktop-directories/ 101 | 16786993 [10304h] /usr/share/xfce4/applications/ 102 | 70254617 [10304h] /home/mikesart/.local/share/ 103 | 70254646 [10304h] /home/mikesart/.config/menus/ 104 | 70254647 [10304h] /home/mikesart/.config/menus/applications-merged/ 105 | 70254684 [10304h] /home/mikesart/.local/share/applications/ 106 | 71048655 [10304h] /home/mikesart/.config/xfce4/terminal/ 107 | 92275589 [10304h] /etc/xdg/menus/ 108 | 94111050 [10304h] /home/mikesart/.config/xfce4/panel/ 109 | 94111468 [10304h] /home/mikesart/.cache/xfce4/xfce4-appfinder/ 110 | ``` 111 | ## Run with Specific Pid(s) 112 | ``` 113 | $ _release/inotify-info 3997 114 | ------------------------------------------------------------------------------ 115 | INotify Limits: 116 | max_queued_events: 16384 117 | max_user_instances: 128 118 | max_user_watches: 65536 119 | ------------------------------------------------------------------------------ 120 | Pid App Watches Instances 121 | 2632 systemd 23 3 122 | 2653 pulseaudio 2 2 123 | 2656 dbus-daemon 2 1 124 | 2987 dbus-daemon 1 1 125 | 3056 xfsettingsd 56 1 126 | 3068 xfdesktop 10 1 127 | 3072 wrapper-2.0 6 1 128 | 3091 xfce4-clipman 1 1 129 | 3099 xiccd 1 1 130 | 3343 xfce4-terminal 1 1 131 | 3997 xfce4-appfinder 11 1 132 | 94111468 [10304h] 15339430 [10304h] 14554799 [10304h] 70254617 [10304h] 70254684 [10304h] 16786993 [10304h] 14551253 [10304h] 14550430 [10304h] 70254647 [10304h] 70254646 [10304h] 133 | 92275589 [10304h] 134 | 4048 xdg-desktop-portal 1 1 135 | 4086 xdg-desktop-portal-gtk 56 1 136 | 205668 vivaldi-bin 8 1 137 | 205705 vivaldi-bin 2 1 138 | ------------------------------------------------------------------------------ 139 | Total inotify Watches: 181 140 | Total inotify Instances: 18 141 | ------------------------------------------------------------------------------ 142 | 143 | Searching '/' for listed inodes... (8 threads) 144 | 14550430 [10304h] /usr/share/applications/ 145 | 14551253 [10304h] /usr/local/share/ 146 | 14554799 [10304h] /usr/share/xfce4/ 147 | 15339430 [10304h] /usr/share/desktop-directories/ 148 | 16786993 [10304h] /usr/share/xfce4/applications/ 149 | 70254617 [10304h] /home/mikesart/.local/share/ 150 | 70254646 [10304h] /home/mikesart/.config/menus/ 151 | 70254647 [10304h] /home/mikesart/.config/menus/applications-merged/ 152 | 70254684 [10304h] /home/mikesart/.local/share/applications/ 153 | 92275589 [10304h] /etc/xdg/menus/ 154 | 94111468 [10304h] /home/mikesart/.cache/xfce4/xfce4-appfinder/ 155 | ``` 156 | 157 | ## Run on Docker/podman 158 | 159 | ```sh 160 | docker build . -t inotify-info 161 | docker run --rm --privileged -v /proc:/proc inotify-info 162 | ``` 163 | 164 | When running under [podman][podman] non-root mode, append `--ulimit 165 | nofile=65535:65535` to the `podman build` command. 166 | 167 | ## Run on Nix(OS) 168 | 169 | ``` 170 | nix run nixpkgs#inotify-info 171 | ``` 172 | 173 | ## Credits 174 | 175 | [lfqueue][lfqueue] is [BSD-2-Clause License][bsd] 176 | 177 | 178 | [problem1]: https://code.visualstudio.com/docs/setup/linux#_visual-studio-code-is-unable-to-watch-for-file-changes-in-this-large-workspace-error-enospc 179 | [problem2]: https://unix.stackexchange.com/questions/15509/whos-consuming-my-inotify-resources 180 | [lfqueue]: https://github.com/Taymindis/lfqueue 181 | [bsd]: https://github.com/Taymindis/lfqueue/blob/master/LICENSE 182 | [podman]: https://podman.io/ 183 | -------------------------------------------------------------------------------- /lfqueue/lfqueue.c: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * BSD 2-Clause License 4 | * 5 | * Copyright (c) 2018, Taymindis Woon 6 | * All rights reserved. 7 | * 8 | * Redistribution and use in source and binary forms, with or without 9 | * modification, are permitted provided that the following conditions are met: 10 | * 11 | * * Redistributions of source code must retain the above copyright notice, this 12 | * list of conditions and the following disclaimer. 13 | * 14 | * * Redistributions in binary form must reproduce the above copyright notice, 15 | * this list of conditions and the following disclaimer in the documentation 16 | * and/or other materials provided with the distribution. 17 | * 18 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | * 29 | */ 30 | #include 31 | #include 32 | #include 33 | #if defined __GNUC__ || defined __CYGWIN__ || defined __MINGW32__ || defined __APPLE__ 34 | 35 | #include 36 | #include 37 | #include // for usleep 38 | 39 | #define __LFQ_VAL_COMPARE_AND_SWAP __sync_val_compare_and_swap 40 | #define __LFQ_BOOL_COMPARE_AND_SWAP __sync_bool_compare_and_swap 41 | #define __LFQ_FETCH_AND_ADD __sync_fetch_and_add 42 | #define __LFQ_ADD_AND_FETCH __sync_add_and_fetch 43 | #define __LFQ_YIELD_THREAD sched_yield 44 | #define __LFQ_SYNC_MEMORY __sync_synchronize 45 | 46 | #else 47 | 48 | #include 49 | #include 50 | #ifdef _WIN64 51 | inline BOOL __SYNC_BOOL_CAS(LONG64 volatile* dest, LONG64 input, LONG64 comparand) 52 | { 53 | return InterlockedCompareExchangeNoFence64(dest, input, comparand) == comparand; 54 | } 55 | #define __LFQ_VAL_COMPARE_AND_SWAP(dest, comparand, input) \ 56 | InterlockedCompareExchangeNoFence64((LONG64 volatile*)dest, (LONG64)input, (LONG64)comparand) 57 | #define __LFQ_BOOL_COMPARE_AND_SWAP(dest, comparand, input) \ 58 | __SYNC_BOOL_CAS((LONG64 volatile*)dest, (LONG64)input, (LONG64)comparand) 59 | #define __LFQ_FETCH_AND_ADD InterlockedExchangeAddNoFence64 60 | #define __LFQ_ADD_AND_FETCH InterlockedAddNoFence64 61 | #define __LFQ_SYNC_MEMORY MemoryBarrier 62 | 63 | #else 64 | #ifndef asm 65 | #define asm __asm 66 | #endif 67 | inline BOOL __SYNC_BOOL_CAS(LONG volatile* dest, LONG input, LONG comparand) 68 | { 69 | return InterlockedCompareExchangeNoFence(dest, input, comparand) == comparand; 70 | } 71 | #define __LFQ_VAL_COMPARE_AND_SWAP(dest, comparand, input) \ 72 | InterlockedCompareExchangeNoFence((LONG volatile*)dest, (LONG)input, (LONG)comparand) 73 | #define __LFQ_BOOL_COMPARE_AND_SWAP(dest, comparand, input) \ 74 | __SYNC_BOOL_CAS((LONG volatile*)dest, (LONG)input, (LONG)comparand) 75 | #define __LFQ_FETCH_AND_ADD InterlockedExchangeAddNoFence 76 | #define __LFQ_ADD_AND_FETCH InterlockedAddNoFence 77 | #define __LFQ_SYNC_MEMORY() asm mfence 78 | 79 | #endif 80 | #include 81 | #define __LFQ_YIELD_THREAD SwitchToThread 82 | #endif 83 | 84 | #include "lfqueue.h" 85 | #define DEF_LFQ_ASSIGNED_SPIN 2048 86 | 87 | #if defined __GNUC__ || defined __CYGWIN__ || defined __MINGW32__ || defined __APPLE__ 88 | #define lfq_time_t long 89 | #define lfq_get_curr_time(_time_sec) \ 90 | struct timeval _time_; \ 91 | gettimeofday(&_time_, NULL); \ 92 | *_time_sec = _time_.tv_sec 93 | #define lfq_diff_time(_etime_, _stime_) _etime_ - _stime_ 94 | #else 95 | #define lfq_time_t time_t 96 | #define lfq_get_curr_time(_time_sec) time(_time_sec) 97 | #define lfq_diff_time(_etime_, _stime_) difftime(_etime_, _stime_) 98 | #endif 99 | 100 | struct lfqueue_cas_node_s { 101 | void* value; 102 | struct lfqueue_cas_node_s *next, *nextfree; 103 | lfq_time_t _deactivate_tm; 104 | }; 105 | 106 | // static lfqueue_cas_node_t* __lfq_assigned(lfqueue_t *); 107 | static void __lfq_recycle_free(lfqueue_t*, lfqueue_cas_node_t*); 108 | static void __lfq_check_free(lfqueue_t*); 109 | static void* _dequeue(lfqueue_t*); 110 | static void* _single_dequeue(lfqueue_t*); 111 | static int _enqueue(lfqueue_t*, void*); 112 | static inline void* _lfqueue_malloc(__attribute__((unused)) void* pl, size_t sz) 113 | { 114 | return malloc(sz); 115 | } 116 | static inline void _lfqueue_free(__attribute__((unused)) void* pl, void* ptr) 117 | { 118 | free(ptr); 119 | } 120 | 121 | static void* 122 | _dequeue(lfqueue_t* lfqueue) 123 | { 124 | lfqueue_cas_node_t *head, *next; 125 | void* val; 126 | 127 | for (;;) { 128 | head = lfqueue->head; 129 | if (__LFQ_BOOL_COMPARE_AND_SWAP(&lfqueue->head, head, head)) { 130 | next = head->next; 131 | if (__LFQ_BOOL_COMPARE_AND_SWAP(&lfqueue->tail, head, head)) { 132 | if (next == NULL) { 133 | val = NULL; 134 | goto _done; 135 | } 136 | } else { 137 | if (next) { 138 | val = next->value; 139 | if (__LFQ_BOOL_COMPARE_AND_SWAP(&lfqueue->head, head, next)) { 140 | break; 141 | } 142 | } else { 143 | val = NULL; 144 | goto _done; 145 | } 146 | } 147 | } 148 | } 149 | 150 | __lfq_recycle_free(lfqueue, head); 151 | _done: 152 | // __asm volatile("" ::: "memory"); 153 | __LFQ_SYNC_MEMORY(); 154 | __lfq_check_free(lfqueue); 155 | return val; 156 | } 157 | 158 | static void* 159 | _single_dequeue(lfqueue_t* lfqueue) 160 | { 161 | lfqueue_cas_node_t *head, *next; 162 | void* val; 163 | 164 | for (;;) { 165 | head = lfqueue->head; 166 | if (__LFQ_BOOL_COMPARE_AND_SWAP(&lfqueue->head, head, head)) { 167 | next = head->next; 168 | if (__LFQ_BOOL_COMPARE_AND_SWAP(&lfqueue->tail, head, head)) { 169 | if (next == NULL) { 170 | return NULL; 171 | } 172 | } else { 173 | if (next) { 174 | val = next->value; 175 | if (__LFQ_BOOL_COMPARE_AND_SWAP(&lfqueue->head, head, next)) { 176 | lfqueue->_free(lfqueue->pl, head); 177 | break; 178 | } 179 | } else { 180 | return NULL; 181 | } 182 | } 183 | } 184 | } 185 | return val; 186 | } 187 | 188 | static int 189 | _enqueue(lfqueue_t* lfqueue, void* value) 190 | { 191 | lfqueue_cas_node_t *tail, *node; 192 | node = (lfqueue_cas_node_t*)lfqueue->_malloc(lfqueue->pl, sizeof(lfqueue_cas_node_t)); 193 | if (node == NULL) { 194 | perror("malloc"); 195 | return errno; 196 | } 197 | node->value = value; 198 | node->next = NULL; 199 | node->nextfree = NULL; 200 | for (;;) { 201 | __LFQ_SYNC_MEMORY(); 202 | tail = lfqueue->tail; 203 | if (__LFQ_BOOL_COMPARE_AND_SWAP(&tail->next, NULL, node)) { 204 | // compulsory swap as tail->next is no NULL anymore, it has fenced on other thread 205 | __LFQ_BOOL_COMPARE_AND_SWAP(&lfqueue->tail, tail, node); 206 | __lfq_check_free(lfqueue); 207 | return 0; 208 | } 209 | } 210 | 211 | /*It never be here*/ 212 | return -1; 213 | } 214 | 215 | static void 216 | __lfq_recycle_free(lfqueue_t* lfqueue, lfqueue_cas_node_t* freenode) 217 | { 218 | lfqueue_cas_node_t* freed; 219 | do { 220 | freed = lfqueue->move_free; 221 | } while (!__LFQ_BOOL_COMPARE_AND_SWAP(&freed->nextfree, NULL, freenode)); 222 | 223 | lfq_get_curr_time(&freenode->_deactivate_tm); 224 | 225 | __LFQ_BOOL_COMPARE_AND_SWAP(&lfqueue->move_free, freed, freenode); 226 | } 227 | 228 | static void 229 | __lfq_check_free(lfqueue_t* lfqueue) 230 | { 231 | lfq_time_t curr_time; 232 | if (__LFQ_BOOL_COMPARE_AND_SWAP(&lfqueue->in_free_mode, 0, 1)) { 233 | lfq_get_curr_time(&curr_time); 234 | lfqueue_cas_node_t *rtfree = lfqueue->root_free, *nextfree; 235 | while (rtfree && (rtfree != lfqueue->move_free)) { 236 | nextfree = rtfree->nextfree; 237 | if (lfq_diff_time(curr_time, rtfree->_deactivate_tm) > 2) { 238 | // printf("%p\n", rtfree); 239 | lfqueue->_free(lfqueue->pl, rtfree); 240 | rtfree = nextfree; 241 | } else { 242 | break; 243 | } 244 | } 245 | lfqueue->root_free = rtfree; 246 | __LFQ_BOOL_COMPARE_AND_SWAP(&lfqueue->in_free_mode, 1, 0); 247 | } 248 | __LFQ_SYNC_MEMORY(); 249 | } 250 | 251 | int lfqueue_init(lfqueue_t* lfqueue) 252 | { 253 | return lfqueue_init_mf(lfqueue, NULL, _lfqueue_malloc, _lfqueue_free); 254 | } 255 | 256 | int lfqueue_init_mf(lfqueue_t* lfqueue, void* pl, lfqueue_malloc_fn lfqueue_malloc, lfqueue_free_fn lfqueue_free) 257 | { 258 | lfqueue->_malloc = lfqueue_malloc; 259 | lfqueue->_free = lfqueue_free; 260 | lfqueue->pl = pl; 261 | 262 | lfqueue_cas_node_t* base = lfqueue_malloc(pl, sizeof(lfqueue_cas_node_t)); 263 | lfqueue_cas_node_t* freebase = lfqueue_malloc(pl, sizeof(lfqueue_cas_node_t)); 264 | if (base == NULL || freebase == NULL) { 265 | perror("malloc"); 266 | return errno; 267 | } 268 | base->value = NULL; 269 | base->next = NULL; 270 | base->nextfree = NULL; 271 | base->_deactivate_tm = 0; 272 | 273 | freebase->value = NULL; 274 | freebase->next = NULL; 275 | freebase->nextfree = NULL; 276 | freebase->_deactivate_tm = 0; 277 | 278 | lfqueue->head = lfqueue->tail = base; // Not yet to be free for first node only 279 | lfqueue->root_free = lfqueue->move_free = freebase; // Not yet to be free for first node only 280 | lfqueue->size = 0; 281 | lfqueue->in_free_mode = 0; 282 | 283 | return 0; 284 | } 285 | 286 | void lfqueue_destroy(lfqueue_t* lfqueue) 287 | { 288 | void* p; 289 | while ((p = lfqueue_deq(lfqueue))) { 290 | lfqueue->_free(lfqueue->pl, p); 291 | } 292 | // Clear the recycle chain nodes 293 | lfqueue_cas_node_t *rtfree = lfqueue->root_free, *nextfree; 294 | while (rtfree && (rtfree != lfqueue->move_free)) { 295 | nextfree = rtfree->nextfree; 296 | lfqueue->_free(lfqueue->pl, rtfree); 297 | rtfree = nextfree; 298 | } 299 | if (rtfree) { 300 | lfqueue->_free(lfqueue->pl, rtfree); 301 | } 302 | 303 | lfqueue->_free(lfqueue->pl, lfqueue->tail); // Last free 304 | 305 | lfqueue->size = 0; 306 | } 307 | 308 | int lfqueue_enq(lfqueue_t* lfqueue, void* value) 309 | { 310 | if (_enqueue(lfqueue, value)) { 311 | return -1; 312 | } 313 | __LFQ_ADD_AND_FETCH(&lfqueue->size, 1); 314 | return 0; 315 | } 316 | 317 | void* lfqueue_deq(lfqueue_t* lfqueue) 318 | { 319 | void* v; 320 | if ( //__LFQ_ADD_AND_FETCH(&lfqueue->size, 0) && 321 | (v = _dequeue(lfqueue))) { 322 | 323 | __LFQ_FETCH_AND_ADD(&lfqueue->size, -1); 324 | return v; 325 | } 326 | return NULL; 327 | } 328 | 329 | void* lfqueue_deq_must(lfqueue_t* lfqueue) 330 | { 331 | void* v; 332 | while (!(v = _dequeue(lfqueue))) { 333 | // Rest the thread for other thread, to avoid keep looping force 334 | lfqueue_sleep(1); 335 | } 336 | __LFQ_FETCH_AND_ADD(&lfqueue->size, -1); 337 | return v; 338 | } 339 | 340 | /**This is only applicable when only single thread consume only**/ 341 | void* lfqueue_single_deq(lfqueue_t* lfqueue) 342 | { 343 | void* v; 344 | if ( //__LFQ_ADD_AND_FETCH(&lfqueue->size, 0) && 345 | (v = _single_dequeue(lfqueue))) { 346 | 347 | __LFQ_FETCH_AND_ADD(&lfqueue->size, -1); 348 | return v; 349 | } 350 | return NULL; 351 | } 352 | 353 | /**This is only applicable when only single thread consume only**/ 354 | void* lfqueue_single_deq_must(lfqueue_t* lfqueue) 355 | { 356 | void* v; 357 | while (!(v = _single_dequeue(lfqueue))) { 358 | // Rest the thread for other thread, to avoid keep looping force 359 | lfqueue_sleep(1); 360 | } 361 | __LFQ_FETCH_AND_ADD(&lfqueue->size, -1); 362 | return v; 363 | } 364 | 365 | size_t 366 | lfqueue_size(lfqueue_t* lfqueue) 367 | { 368 | return __LFQ_ADD_AND_FETCH(&lfqueue->size, 0); 369 | } 370 | 371 | void lfqueue_sleep(unsigned int milisec) 372 | { 373 | #if defined __GNUC__ || defined __CYGWIN__ || defined __MINGW32__ || defined __APPLE__ 374 | #pragma GCC diagnostic push 375 | #pragma GCC diagnostic ignored "-Wimplicit-function-declaration" 376 | usleep(milisec * 1000); 377 | #pragma GCC diagnostic pop 378 | #else 379 | Sleep(milisec); 380 | #endif 381 | } 382 | 383 | #ifdef __cplusplus 384 | } 385 | #endif 386 | -------------------------------------------------------------------------------- /inotify-info.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Michael Sartain 3 | * 4 | * All Rights Reserved. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | #define _GNU_SOURCE 1 26 | 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | #include 44 | 45 | #include 46 | #include 47 | #include 48 | #include 49 | #include 50 | #include 51 | 52 | #include "inotify-info.h" 53 | #include "lfqueue/lfqueue.h" 54 | 55 | #ifndef INOTIFYINFO_VERSION 56 | #error INOTIFYINFO_VERSION must be set 57 | #endif 58 | 59 | static int g_verbose = 0; 60 | static size_t g_numthreads = 32; 61 | static std::string g_search_path = "/"; // Default path to root 62 | static int g_short_stat = 0; // Quick overview: limits + totals only 63 | static int g_sort_by_instances = 0; // Sort list by instances instead of watches 64 | 65 | /* true if at least one inotify watch is found in fdinfo files 66 | * On a system with no active inotify watches, but which otherwise 67 | * supports inotify watch info, this will prevent the watches column 68 | * from being displayed. 69 | * This case is indistinguishable from the case where the kernel does 70 | * not support inotify watch info. 71 | */ 72 | static int g_kernel_provides_watches_info = 0; 73 | 74 | static char thousands_sep = ','; 75 | 76 | static std::vector ignore_dirs; 77 | 78 | const char* RESET = "\x1b[0m"; 79 | const char* YELLOW = "\x1b[0;33m"; 80 | const char* CYAN = "\x1b[0;36m"; 81 | const char* BGRAY = "\x1b[1;30m"; 82 | const char* BGREEN = "\x1b[1;32m"; 83 | const char* BYELLOW = "\x1b[1;33m"; 84 | const char* BCYAN = "\x1b[1;36m"; 85 | 86 | void set_no_color() 87 | { 88 | RESET = ""; 89 | YELLOW = ""; 90 | CYAN = ""; 91 | BGRAY = ""; 92 | BGREEN = ""; 93 | BYELLOW = ""; 94 | BCYAN = ""; 95 | } 96 | 97 | /* 98 | * filename info 99 | */ 100 | struct filename_info_t { 101 | ino64_t inode; // Inode number 102 | dev_t dev; // Device ID containing file 103 | std::string filename; 104 | }; 105 | 106 | /* 107 | * inotify process info 108 | */ 109 | struct procinfo_t { 110 | pid_t pid = 0; 111 | 112 | // uid 113 | uid_t uid = 0; 114 | 115 | // Count of inotify watches and instances 116 | uint32_t watches = 0; 117 | uint32_t instances = 0; 118 | 119 | // This appname or pid found in command line? 120 | bool in_cmd_line = false; 121 | 122 | // Full executable path 123 | std::string executable; 124 | 125 | // Executable basename 126 | std::string appname; 127 | 128 | // Inotify fdset filenames 129 | std::vector fdset_filenames; 130 | 131 | // Device id map -> set of inodes for that device id 132 | std::unordered_map> dev_map; 133 | }; 134 | 135 | class lfqueue_wrapper_t { 136 | public: 137 | lfqueue_wrapper_t() { lfqueue_init(&queue); } 138 | ~lfqueue_wrapper_t() { lfqueue_destroy(&queue); } 139 | 140 | void queue_directory(char* path) { lfqueue_enq(&queue, path); } 141 | char* dequeue_directory() { return (char*)lfqueue_deq(&queue); } 142 | 143 | public: 144 | typedef long long my_m256i __attribute__((__vector_size__(32), __aligned__(32))); 145 | 146 | union { 147 | lfqueue_t queue; 148 | my_m256i align_buf[4]; // Align to 128 bytes 149 | }; 150 | }; 151 | 152 | /* 153 | * shared thread data 154 | */ 155 | class thread_shared_data_t { 156 | public: 157 | bool init(uint32_t numthreads, const std::vector& inotify_proclist); 158 | 159 | public: 160 | // Array of queues - one per thread 161 | std::vector dirqueues; 162 | // Map of all inotify inodes watched to the set of devices they are on 163 | std::unordered_map> inode_set; 164 | }; 165 | 166 | /* 167 | * thread info 168 | */ 169 | class thread_info_t { 170 | public: 171 | thread_info_t(thread_shared_data_t& tdata_in) 172 | : tdata(tdata_in) 173 | { 174 | } 175 | ~thread_info_t() { } 176 | 177 | void queue_directory(char* path); 178 | char* dequeue_directory(); 179 | 180 | // Returns -1: queue empty, 0: open error, > 0 success 181 | int parse_dirqueue_entry(); 182 | 183 | void add_filename(ino64_t inode, const char* path, const char* d_name, bool is_dir); 184 | 185 | public: 186 | uint32_t idx = 0; 187 | pthread_t pthread_id = 0; 188 | 189 | thread_shared_data_t& tdata; 190 | 191 | // Total dirs scanned by this thread 192 | uint32_t scanned_dirs = 0; 193 | // Files found by this thread 194 | std::vector found_files; 195 | }; 196 | 197 | /* 198 | * getdents64 syscall 199 | */ 200 | GCC_DIAG_PUSH_OFF(pedantic) 201 | struct linux_dirent64 { 202 | uint64_t d_ino; // Inode number 203 | int64_t d_off; // Offset to next linux_dirent 204 | unsigned short d_reclen; // Length of this linux_dirent 205 | unsigned char d_type; // File type 206 | char d_name[]; // Filename (null-terminated) 207 | }; 208 | GCC_DIAG_POP() 209 | 210 | int sys_getdents64(int fd, char* dirp, int count) 211 | { 212 | return syscall(SYS_getdents64, fd, dirp, count); 213 | } 214 | 215 | static double gettime() 216 | { 217 | struct timespec ts; 218 | 219 | clock_gettime(CLOCK_MONOTONIC, &ts); 220 | return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9; 221 | } 222 | 223 | std::string string_formatv(const char* fmt, va_list ap) 224 | { 225 | std::string str; 226 | int size = 512; 227 | 228 | for (;;) { 229 | str.resize(size); 230 | int n = vsnprintf((char*)str.c_str(), size, fmt, ap); 231 | 232 | if ((n > -1) && (n < size)) { 233 | str.resize(n); 234 | return str; 235 | } 236 | 237 | size = (n > -1) ? (n + 1) : (size * 2); 238 | } 239 | } 240 | 241 | std::string string_format(const char* fmt, ...) 242 | { 243 | va_list ap; 244 | std::string str; 245 | 246 | va_start(ap, fmt); 247 | str = string_formatv(fmt, ap); 248 | va_end(ap); 249 | 250 | return str; 251 | } 252 | 253 | static std::string get_link_name(const char* pathname) 254 | { 255 | std::string Result; 256 | char filename[PATH_MAX + 1]; 257 | 258 | ssize_t ret = readlink(pathname, filename, sizeof(filename)); 259 | if ((ret > 0) && (ret < (ssize_t)sizeof(filename))) { 260 | filename[ret] = 0; 261 | Result = filename; 262 | } 263 | return Result; 264 | } 265 | 266 | static uid_t get_uid(const char* pathname) 267 | { 268 | int fd = open(pathname, O_RDONLY, 0); 269 | 270 | if (fd >= 0) { 271 | char buf[16 * 1024]; 272 | 273 | ssize_t len = read(fd, buf, sizeof(buf)); 274 | 275 | close(fd); 276 | fd = -1; 277 | 278 | if (len > 0) { 279 | buf[len - 1] = 0; 280 | 281 | const char* uidstr = strstr(buf, "\nUid:"); 282 | if (uidstr) { 283 | return atoll(uidstr + 5); 284 | } 285 | } 286 | } 287 | 288 | return -1; 289 | } 290 | 291 | static uint64_t get_token_val(const char* line, const char* token) 292 | { 293 | const char* str = strstr(line, token); 294 | 295 | return str ? strtoull(str + strlen(token), nullptr, 16) : 0; 296 | } 297 | 298 | static uint32_t inotify_parse_fdinfo_file(procinfo_t& procinfo, const char* fdset_name) 299 | { 300 | uint32_t watch_count = 0; 301 | 302 | FILE* fp = fopen(fdset_name, "r"); 303 | if (fp) { 304 | char line_buf[256]; 305 | 306 | procinfo.fdset_filenames.push_back(fdset_name); 307 | 308 | for (;;) { 309 | if (!fgets(line_buf, sizeof(line_buf), fp)) 310 | break; 311 | 312 | /* sample fdinfo; inotify line added in linux 3.8, available if 313 | * kernel compiled with CONFIG_INOTIFY_USER and CONFIG_PROC_FS 314 | * pos: 0 315 | * flags: 00 316 | * mnt_id: 15 317 | * ino: 5865 318 | * inotify wd:1 ino:80001 sdev:800011 mask:100 ignored_mask:0 fhandle-bytes:8 fhandle-type:1 f_handle:01000800bc1b8c7c 319 | */ 320 | if (!strncmp(line_buf, "inotify ", 8)) { 321 | watch_count++; 322 | 323 | uint64_t inode_val = get_token_val(line_buf, "ino:"); 324 | uint64_t sdev_val = get_token_val(line_buf, "sdev:"); 325 | 326 | if (inode_val) { 327 | // https://unix.stackexchange.com/questions/645937/listing-the-files-that-are-being-watched-by-inotify-instances 328 | // Assuming that the sdev field is encoded according to Linux's so-called "huge 329 | // encoding", which uses 20 bits (instead of 8) for minor numbers, in bitwise 330 | // parlance the major number is sdev >> 20 while the minor is sdev & 0xfffff. 331 | unsigned int major = sdev_val >> 20; 332 | unsigned int minor = sdev_val & 0xfffff; 333 | 334 | // Add inode to this device map 335 | procinfo.dev_map[makedev(major, minor)].insert(inode_val); 336 | } 337 | } 338 | } 339 | 340 | fclose(fp); 341 | } 342 | 343 | return watch_count; 344 | } 345 | 346 | static void inotify_parse_fddir(procinfo_t& procinfo) 347 | { 348 | std::string filename = string_format("/proc/%d/fd", procinfo.pid); 349 | 350 | DIR* dir_fd = opendir(filename.c_str()); 351 | if (!dir_fd) 352 | return; 353 | 354 | for (;;) { 355 | struct dirent* dp_fd = readdir(dir_fd); 356 | if (!dp_fd) 357 | break; 358 | 359 | if ((dp_fd->d_type == DT_LNK) && isdigit(dp_fd->d_name[0])) { 360 | filename = string_format("/proc/%d/fd/%s", procinfo.pid, dp_fd->d_name); 361 | filename = get_link_name(filename.c_str()); 362 | 363 | if (filename == "anon_inode:inotify" || filename == "inotify") { 364 | filename = string_format("/proc/%d/fdinfo/%s", procinfo.pid, dp_fd->d_name); 365 | 366 | procinfo.instances++; 367 | procinfo.watches += inotify_parse_fdinfo_file(procinfo, filename.c_str()); 368 | 369 | /* If any watches have been found, enable the stats display */ 370 | g_kernel_provides_watches_info |= !!procinfo.watches; 371 | } 372 | } 373 | } 374 | 375 | closedir(dir_fd); 376 | } 377 | 378 | void thread_info_t::queue_directory(char* path) 379 | { 380 | tdata.dirqueues[idx].queue_directory(path); 381 | } 382 | 383 | char* thread_info_t::dequeue_directory() 384 | { 385 | char* path = tdata.dirqueues[idx].dequeue_directory(); 386 | 387 | if (!path) { 388 | // Nothing on our queue, check queues on other threads 389 | for (lfqueue_wrapper_t& dirq : tdata.dirqueues) { 390 | path = dirq.dequeue_directory(); 391 | if (path) 392 | break; 393 | } 394 | } 395 | 396 | return path; 397 | } 398 | 399 | // statx() was added to Linux in kernel 4.11; library support was added in glibc 2.28. 400 | #if defined(__linux__) && ((__GLIBC__ >= 2 && __GLIBC_MINOR__ >= 28) || (__GLIBC__ > 2)) 401 | 402 | struct statx mystatx(const char* filename, unsigned int mask = 0) 403 | { 404 | struct statx statxbuf; 405 | int flags = AT_NO_AUTOMOUNT | AT_SYMLINK_NOFOLLOW | AT_STATX_DONT_SYNC; 406 | 407 | if (statx(0, filename, flags, mask, &statxbuf) == -1) { 408 | printf("ERROR: statx-ino( %s ) failed. Errno: %d (%s)\n", filename, errno, strerror(errno)); 409 | memset(&statxbuf, 0, sizeof(statxbuf)); 410 | } 411 | 412 | return statxbuf; 413 | } 414 | 415 | static dev_t stat_get_dev_t(const char* filename) 416 | { 417 | struct statx statxbuf = mystatx(filename); 418 | 419 | return makedev(statxbuf.stx_dev_major, statxbuf.stx_dev_minor); 420 | } 421 | 422 | static uint64_t stat_get_ino(const char* filename) 423 | { 424 | return mystatx(filename, STATX_INO).stx_ino; 425 | } 426 | 427 | #else 428 | 429 | // Fall back to using stat() functions. Should work but be slower than using statx(). 430 | 431 | static dev_t stat_get_dev_t(const char* filename) 432 | { 433 | struct stat statbuf; 434 | 435 | int ret = stat(filename, &statbuf); 436 | if (ret == -1) { 437 | printf("ERROR: stat-dev_t( %s ) failed. Errno: %d (%s)\n", filename, errno, strerror(errno)); 438 | return 0; 439 | } 440 | return statbuf.st_dev; 441 | } 442 | 443 | static uint64_t stat_get_ino(const char* filename) 444 | { 445 | struct stat statbuf; 446 | 447 | int ret = stat(filename, &statbuf); 448 | if (ret == -1) { 449 | printf("ERROR: stat-ino( %s ) failed. Errno: %d (%s)\n", filename, errno, strerror(errno)); 450 | return 0; 451 | } 452 | 453 | return statbuf.st_ino; 454 | } 455 | 456 | #endif 457 | 458 | void thread_info_t::add_filename(ino64_t inode, const char* path, const char* d_name, bool is_dir) 459 | { 460 | auto it = tdata.inode_set.find(inode); 461 | 462 | if (it != tdata.inode_set.end()) { 463 | const std::unordered_set& dev_set = it->second; 464 | 465 | std::string filename = std::string(path) + d_name; 466 | dev_t dev = stat_get_dev_t(filename.c_str()); 467 | 468 | // Make sure the inode AND device ID match before adding. 469 | if (dev_set.find(dev) != dev_set.end()) { 470 | filename_info_t fname; 471 | 472 | fname.filename = is_dir ? filename + "/" : filename; 473 | fname.inode = inode; 474 | fname.dev = dev; 475 | 476 | found_files.push_back(fname); 477 | } 478 | } 479 | } 480 | 481 | static bool is_dot_dir(const char* dname) 482 | { 483 | if (dname[0] == '.') { 484 | if (!dname[1]) 485 | return true; 486 | 487 | if ((dname[1] == '.') && !dname[2]) 488 | return true; 489 | } 490 | 491 | return false; 492 | } 493 | 494 | // From "linux/magic.h" 495 | #define PROC_SUPER_MAGIC 0x9fa0 496 | #define SMB_SUPER_MAGIC 0x517B 497 | #define CIFS_SUPER_MAGIC 0xFF534D42 /* the first four bytes of SMB PDUs */ 498 | #define SMB2_SUPER_MAGIC 0xFE534D42 499 | #define FUSE_SUPER_MAGIC 0x65735546 500 | 501 | // Detect proc and fuse directories and skip them. 502 | // https://github.com/mikesart/inotify-info/issues/6 503 | // Could use setmntent("/proc/mounts", "r") + getmntent if speed is an issue? 504 | static bool is_proc_dir(const char* path, const char* d_name) 505 | { 506 | struct statfs s; 507 | std::string filename = std::string(path) + d_name; 508 | 509 | if (statfs(filename.c_str(), &s) == 0) { 510 | switch (s.f_type) { 511 | case PROC_SUPER_MAGIC: 512 | case FUSE_SUPER_MAGIC: 513 | return true; 514 | } 515 | } 516 | 517 | return false; 518 | } 519 | 520 | // Returns -1: queue empty, 0: open error, > 0 success 521 | int thread_info_t::parse_dirqueue_entry() 522 | { 523 | char __attribute__((aligned(16))) buf[1024]; 524 | 525 | char* path = dequeue_directory(); 526 | if (!path) { 527 | return -1; 528 | } 529 | 530 | for (std::string& dname : ignore_dirs) { 531 | if (dname == path) { 532 | if (g_verbose > 1) { 533 | printf("Ignoring '%s'\n", path); 534 | } 535 | return 0; 536 | } 537 | } 538 | 539 | int fd = open(path, O_RDONLY | O_DIRECTORY, 0); 540 | if (fd < 0) { 541 | free(path); 542 | return 0; 543 | } 544 | 545 | scanned_dirs++; 546 | 547 | size_t pathlen = strlen(path); 548 | 549 | for (;;) { 550 | int ret = sys_getdents64(fd, buf, sizeof(buf)); 551 | 552 | if (ret < 0) { 553 | bool spew_error = true; 554 | 555 | if ((errno == 5) && !strncmp(path, "/sys/kernel/", 12)) { 556 | // In docker container we can get permission denied errors in /sys/kernel. Ignore them. 557 | // https://github.com/mikesart/inotify-info/issues/16 558 | spew_error = false; 559 | } 560 | 561 | if (spew_error) { 562 | printf("ERROR: sys_getdents64 failed on '%s': %d errno: %d (%s)\n", path, ret, errno, strerror(errno)); 563 | } 564 | break; 565 | } 566 | if (ret == 0) 567 | break; 568 | 569 | for (int bpos = 0; bpos < ret;) { 570 | struct linux_dirent64* dirp = (struct linux_dirent64*)(buf + bpos); 571 | const char* d_name = dirp->d_name; 572 | 573 | // DT_BLK This is a block device. 574 | // DT_CHR This is a character device. 575 | // DT_FIFO This is a named pipe (FIFO). 576 | // DT_SOCK This is a UNIX domain socket. 577 | // DT_UNKNOWN The file type could not be determined. 578 | 579 | // DT_REG This is a regular file. 580 | // DT_LNK This is a symbolic link. 581 | if (dirp->d_type == DT_REG || dirp->d_type == DT_LNK) { 582 | add_filename(dirp->d_ino, path, d_name, false); 583 | } 584 | // DT_DIR This is a directory. 585 | else if (dirp->d_type == DT_DIR) { 586 | if (!is_dot_dir(d_name) && !is_proc_dir(path, d_name)) { 587 | add_filename(dirp->d_ino, path, d_name, true); 588 | 589 | size_t len = strlen(d_name); 590 | char* newpath = (char*)malloc(pathlen + len + 2); 591 | 592 | if (newpath) { 593 | strcpy(newpath, path); 594 | strcpy(newpath + pathlen, d_name); 595 | newpath[pathlen + len] = '/'; 596 | newpath[pathlen + len + 1] = 0; 597 | 598 | queue_directory(newpath); 599 | } 600 | } 601 | } 602 | 603 | bpos += dirp->d_reclen; 604 | } 605 | } 606 | 607 | close(fd); 608 | free(path); 609 | return 1; 610 | } 611 | 612 | static void* parse_dirqueue_threadproc(void* arg) 613 | { 614 | thread_info_t* pthread_info = (thread_info_t*)arg; 615 | 616 | for (;;) { 617 | // Loop until all the dequeue(s) fail 618 | if (pthread_info->parse_dirqueue_entry() == -1) 619 | break; 620 | } 621 | 622 | return nullptr; 623 | } 624 | 625 | static bool is_proc_in_cmdline_applist(const procinfo_t& procinfo, std::vector& cmdline_applist) 626 | { 627 | for (const std::string& str : cmdline_applist) { 628 | // Check if our command line string is a subset of this appname 629 | if (strstr(procinfo.appname.c_str(), str.c_str())) 630 | return true; 631 | 632 | // Check if the PIDs match 633 | if (atoll(str.c_str()) == procinfo.pid) 634 | return true; 635 | } 636 | 637 | return false; 638 | } 639 | 640 | static bool watch_count_is_greater(procinfo_t elem1, procinfo_t elem2) 641 | { 642 | return elem1.watches > elem2.watches; 643 | } 644 | 645 | static bool instance_count_is_greater(procinfo_t elem1, procinfo_t elem2) 646 | { 647 | return elem1.instances > elem2.instances; 648 | } 649 | 650 | static bool init_inotify_proclist(std::vector& inotify_proclist) 651 | { 652 | DIR* dir_proc = opendir("/proc"); 653 | 654 | if (!dir_proc) { 655 | printf("ERROR: opendir /proc failed: %d (%s)\n", errno, strerror(errno)); 656 | return false; 657 | } 658 | 659 | for (;;) { 660 | struct dirent* dp_proc = readdir(dir_proc); 661 | if (!dp_proc) 662 | break; 663 | 664 | if ((dp_proc->d_type == DT_DIR) && isdigit(dp_proc->d_name[0])) { 665 | procinfo_t procinfo; 666 | 667 | procinfo.pid = atoll(dp_proc->d_name); 668 | 669 | std::string executable = string_format("/proc/%d/exe", procinfo.pid); 670 | std::string status = string_format("/proc/%d/status", procinfo.pid); 671 | procinfo.uid = get_uid(status.c_str()); 672 | procinfo.executable = get_link_name(executable.c_str()); 673 | if (!procinfo.executable.empty()) { 674 | procinfo.appname = basename((char*)procinfo.executable.c_str()); 675 | 676 | inotify_parse_fddir(procinfo); 677 | 678 | if (procinfo.instances) { 679 | inotify_proclist.push_back(procinfo); 680 | } 681 | } 682 | } 683 | } 684 | if (g_sort_by_instances) 685 | std::sort(inotify_proclist.begin(), inotify_proclist.end(), instance_count_is_greater); 686 | else 687 | std::sort(inotify_proclist.begin(), inotify_proclist.end(), watch_count_is_greater); 688 | 689 | closedir(dir_proc); 690 | return true; 691 | } 692 | 693 | // From: 694 | // https://stackoverflow.com/questions/1449805/how-to-format-a-number-using-comma-as-thousands-separator-in-c 695 | size_t str_format_uint32(char dst[16], uint32_t num) 696 | { 697 | if (thousands_sep) { 698 | char src[16]; 699 | char* p_src = src; 700 | char* p_dst = dst; 701 | int num_len, commas; 702 | 703 | num_len = sprintf(src, "%u", num); 704 | 705 | for (commas = 2 - num_len % 3; *p_src; commas = (commas + 1) % 3) { 706 | *p_dst++ = *p_src++; 707 | if (commas == 1) { 708 | *p_dst++ = thousands_sep; 709 | } 710 | } 711 | *--p_dst = '\0'; 712 | 713 | return (size_t)(p_dst - dst); 714 | } 715 | 716 | return sprintf(dst, "%u", num); 717 | } 718 | 719 | static void print_inotify_proclist(std::vector& inotify_proclist) 720 | { 721 | #if 0 722 | // test data 723 | procinfo_t proc_info = {}; 724 | proc_info.pid = 100; 725 | proc_info.appname = "fsnotifier"; 726 | proc_info.watches = 2; 727 | proc_info.instances = 1; 728 | inotify_proclist.push_back(proc_info); 729 | 730 | proc_info.pid = 1000; 731 | proc_info.appname = "evolution-addressbook-factor"; 732 | proc_info.watches = 116; 733 | proc_info.instances = 10; 734 | inotify_proclist.push_back(proc_info); 735 | 736 | proc_info.pid = 22154; 737 | proc_info.appname = "evolution-addressbook-factor blah blah"; 738 | proc_info.watches = 28200; 739 | proc_info.instances = 100; 740 | inotify_proclist.push_back(proc_info); 741 | 742 | proc_info.pid = 0x7fffffff; 743 | proc_info.appname = "evolution-addressbook-factor blah blah2"; 744 | proc_info.watches = 999999; 745 | proc_info.instances = 999999999; 746 | inotify_proclist.push_back(proc_info); 747 | #endif 748 | 749 | int lenPid = 10; 750 | int lenUid = 10; 751 | int lenApp = 10; 752 | int lenWatches = 8; 753 | int lenInstances = 10; 754 | 755 | for (procinfo_t& procinfo : inotify_proclist) 756 | lenApp = std::max(procinfo.appname.length(), lenApp); 757 | 758 | /* If the number of watches is negative, the kernel doesn't support this info. omit the header*/ 759 | if (g_kernel_provides_watches_info) 760 | printf("%s%*s %-*s %-*s %*s %*s%s\n", 761 | BCYAN, lenPid, "Pid", lenUid, "Uid", lenApp, "App", lenWatches, "Watches", lenInstances, "Instances", RESET); 762 | else 763 | printf("%s%*s %-*s %*s %*s%s\n", 764 | BCYAN, lenPid, "Pid", lenUid, "Uid", lenApp, "App", lenInstances, "Instances", RESET); 765 | 766 | for (procinfo_t& procinfo : inotify_proclist) { 767 | char watches_str[16]; 768 | 769 | str_format_uint32(watches_str, procinfo.watches); 770 | 771 | if (g_kernel_provides_watches_info) 772 | printf("%*d %-*d %s%-*s%s %*s %*u\n", 773 | lenPid, procinfo.pid, 774 | lenUid, procinfo.uid, 775 | BYELLOW, lenApp, procinfo.appname.c_str(), RESET, 776 | lenWatches, watches_str, 777 | lenInstances, procinfo.instances); 778 | else 779 | printf("%*d %-*d %s%-*s%s %*u\n", 780 | lenPid, procinfo.pid, 781 | lenUid, procinfo.uid, 782 | BYELLOW, lenApp, procinfo.appname.c_str(), RESET, 783 | lenInstances, procinfo.instances); 784 | 785 | if (g_verbose > 1) { 786 | for (std::string& fname : procinfo.fdset_filenames) { 787 | printf(" %s%s%s\n", CYAN, fname.c_str(), RESET); 788 | } 789 | } 790 | 791 | if (procinfo.in_cmd_line) { 792 | for (const auto& it1 : procinfo.dev_map) { 793 | dev_t dev = it1.first; 794 | 795 | printf("%s[%u.%u]:%s", BGRAY, major(dev), minor(dev), RESET); 796 | for (const auto& it2 : it1.second) { 797 | std::string inode_device_str = string_format("%lu", it2); 798 | 799 | printf(" %s%s%s", BGRAY, inode_device_str.c_str(), RESET); 800 | } 801 | printf("\n"); 802 | } 803 | } 804 | } 805 | } 806 | 807 | bool thread_shared_data_t::init(uint32_t numthreads, const std::vector& inotify_proclist) 808 | { 809 | for (const procinfo_t& procinfo : inotify_proclist) { 810 | if (!procinfo.in_cmd_line) 811 | continue; 812 | 813 | for (const auto& it1 : procinfo.dev_map) { 814 | dev_t dev = it1.first; 815 | 816 | for (const auto& inode : it1.second) { 817 | inode_set[inode].insert(dev); 818 | } 819 | } 820 | } 821 | 822 | if (!inode_set.empty()) { 823 | dirqueues.resize(numthreads); 824 | } 825 | 826 | return !inode_set.empty(); 827 | } 828 | 829 | static uint32_t find_files_in_inode_set(const std::vector& inotify_proclist, 830 | std::vector& all_found_files) 831 | { 832 | thread_shared_data_t tdata; 833 | 834 | g_numthreads = std::max(1, g_numthreads); 835 | 836 | if (!tdata.init(g_numthreads, inotify_proclist)) 837 | return 0; 838 | printf("\n%sSearching '%s' for listed inodes...%s (%lu threads)\n", 839 | BCYAN, g_search_path.c_str(), RESET, g_numthreads); 840 | 841 | // Initialize thread_info_t array 842 | std::vector thread_array(g_numthreads, thread_info_t(tdata)); 843 | 844 | for (uint32_t idx = 0; idx < thread_array.size(); idx++) { 845 | thread_info_t& thread_info = thread_array[idx]; 846 | 847 | thread_info.idx = idx; 848 | 849 | if (idx == 0) { 850 | // Add search dir in case someone is watching it 851 | thread_info.add_filename(stat_get_ino(g_search_path.c_str()), g_search_path.c_str(), "", false); 852 | // Add and parse root 853 | thread_info.queue_directory(strdup(g_search_path.c_str())); 854 | thread_info.parse_dirqueue_entry(); 855 | } else if (pthread_create(&thread_info.pthread_id, NULL, &parse_dirqueue_threadproc, &thread_info)) { 856 | printf("Warning: pthread_create failed. errno: %d\n", errno); 857 | thread_info.pthread_id = 0; 858 | } 859 | } 860 | 861 | // Put main thread to work 862 | parse_dirqueue_threadproc(&thread_array[0]); 863 | 864 | uint32_t total_scanned_dirs = 0; 865 | for (const thread_info_t& thread_info : thread_array) { 866 | if (thread_info.pthread_id) { 867 | if (g_verbose > 1) { 868 | printf("Waiting for thread #%zu\n", thread_info.pthread_id); 869 | } 870 | 871 | void* status = NULL; 872 | int rc = pthread_join(thread_info.pthread_id, &status); 873 | 874 | if (g_verbose > 1) { 875 | printf("Thread #%zu rc=%d status=%d\n", thread_info.pthread_id, rc, (int)(intptr_t)status); 876 | } 877 | } 878 | 879 | // Snag data from this thread 880 | total_scanned_dirs += thread_info.scanned_dirs; 881 | 882 | all_found_files.insert(all_found_files.end(), 883 | thread_info.found_files.begin(), thread_info.found_files.end()); 884 | 885 | if (g_verbose > 1) { 886 | printf("Thread #%zu: %u dirs, %zu files found\n", 887 | thread_info.pthread_id, thread_info.scanned_dirs, thread_info.found_files.size()); 888 | } 889 | } 890 | 891 | struct 892 | { 893 | bool operator()(const filename_info_t& a, const filename_info_t& b) const 894 | { 895 | if (a.dev == b.dev) 896 | return a.inode < b.inode; 897 | return a.dev < b.dev; 898 | } 899 | } filename_info_less_func; 900 | 901 | std::sort(all_found_files.begin(), all_found_files.end(), filename_info_less_func); 902 | 903 | return total_scanned_dirs; 904 | } 905 | 906 | static uint32_t get_inotify_procfs_value(const std::string& fname) 907 | { 908 | char buf[64]; 909 | uint32_t val = 0; 910 | std::string filename = "/proc/sys/fs/inotify/" + fname; 911 | 912 | int fd = open(filename.c_str(), O_RDONLY); 913 | if (fd >= 0) { 914 | if (read(fd, buf, sizeof(buf)) > 0) { 915 | val = strtoul(buf, nullptr, 10); 916 | } 917 | 918 | close(fd); 919 | } 920 | 921 | return val; 922 | } 923 | 924 | static void print_inotify_limits() 925 | { 926 | const std::vector filenames = { 927 | "max_queued_events", 928 | "max_user_instances", 929 | "max_user_watches" 930 | }; 931 | 932 | printf("%sINotify Limits:%s\n", BCYAN, RESET); 933 | for (const std::string& fname : filenames) { 934 | char str[16]; 935 | uint32_t val = get_inotify_procfs_value(fname); 936 | 937 | str_format_uint32(str, val); 938 | 939 | printf(" %-20s %s%s%s\n", fname.c_str(), BGREEN, str, RESET); 940 | } 941 | } 942 | 943 | static uint32_t parse_config_file(const char* config_file) 944 | { 945 | uint32_t dir_count = 0; 946 | 947 | FILE* fp = fopen(config_file, "r"); 948 | if (fp) { 949 | char line_buf[8192]; 950 | bool in_ignore_dirs_section = false; 951 | 952 | for (;;) { 953 | if (!fgets(line_buf, sizeof(line_buf) - 1, fp)) 954 | break; 955 | 956 | if (line_buf[0] == '#') { 957 | // comment 958 | } else if (!in_ignore_dirs_section) { 959 | size_t len = strcspn(line_buf, "\r\n"); 960 | 961 | if ((len == 12) && !strncmp("[ignoredirs]", line_buf, 12)) { 962 | in_ignore_dirs_section = true; 963 | } 964 | } else if (line_buf[0] == '[') { 965 | in_ignore_dirs_section = false; 966 | } else if (in_ignore_dirs_section && (line_buf[0] == '/')) { 967 | size_t len = strcspn(line_buf, "\r\n"); 968 | 969 | if (len > 1) { 970 | line_buf[len] = 0; 971 | if (line_buf[len - 1] != '/') { 972 | line_buf[len] = '/'; 973 | line_buf[len + 1] = '\0'; 974 | } 975 | 976 | ignore_dirs.push_back(line_buf); 977 | dir_count++; 978 | } 979 | } 980 | } 981 | 982 | fclose(fp); 983 | } 984 | 985 | return dir_count; 986 | } 987 | 988 | static bool parse_ignore_dirs_file() 989 | { 990 | const std::string filename = "inotify-info.config"; 991 | 992 | const char* xdg_config_dir = getenv("XDG_CONFIG_HOME"); 993 | if (xdg_config_dir) { 994 | std::string config_file = std::string(xdg_config_dir) + "/" + filename; 995 | if (parse_config_file(config_file.c_str())) 996 | return true; 997 | 998 | config_file = std::string(xdg_config_dir) + "/.config/" + filename; 999 | if (parse_config_file(config_file.c_str())) 1000 | return true; 1001 | } 1002 | 1003 | const char* home_dir = getenv("HOME"); 1004 | if (home_dir) { 1005 | std::string config_file = std::string(home_dir) + "/" + filename; 1006 | if (parse_config_file(config_file.c_str())) 1007 | return true; 1008 | } 1009 | 1010 | std::string config_file = "/etc/" + filename; 1011 | if (parse_config_file(config_file.c_str())) 1012 | return true; 1013 | 1014 | return false; 1015 | } 1016 | 1017 | void parse_search_dir() 1018 | { 1019 | // Check if the path is a valid directory 1020 | struct stat path_stat; 1021 | if (stat(g_search_path.c_str(), &path_stat) == 0) { 1022 | if (S_ISDIR(path_stat.st_mode)) { 1023 | // Ensure the g_search_path ends with "/" 1024 | if (g_search_path.back() != '/') { 1025 | g_search_path += '/'; 1026 | } 1027 | } else { 1028 | // g_search_path exists but is not a directory 1029 | printf("ERROR: path (%s) is not a directory. Errno: %d (%s)\n", g_search_path.c_str(), errno, strerror(errno)); 1030 | exit(1); 1031 | } 1032 | } else { 1033 | // g_search_path does not exist 1034 | printf("ERROR: path (%s) does not exist. Errno: %d (%s)\n", g_search_path.c_str(), errno, strerror(errno)); 1035 | exit(1); 1036 | } 1037 | } 1038 | 1039 | static void print_version() 1040 | { 1041 | printf("%s\n", INOTIFYINFO_VERSION); 1042 | } 1043 | 1044 | static void print_usage(const char* appname) 1045 | { 1046 | printf("Usage: %s [options] [appname | pid...]\n", appname); 1047 | printf("Where options are:\n"); 1048 | printf(" [--threads=##] Number of threads\n"); 1049 | printf(" [-p=PATH]\n"); 1050 | printf(" [--path=PATH] Path to search (default '/')\n"); 1051 | printf(" [--ignoredir=NAME] Directories to ignore in searched path\n"); 1052 | printf(" [-s|--short-stat] Quick overview: show limits and totals only\n"); 1053 | printf(" [--sort-by-instances] Sort list by instances instead of watches\n"); 1054 | printf(" [-v|--verbose] More option increase verbosity level\n"); 1055 | printf(" [--no-color] Do not colorize output\n"); 1056 | printf(" [--version] Show version and stop\n"); 1057 | printf(" [-?|-h|--help] Show this help and stop\n"); 1058 | } 1059 | 1060 | static void parse_cmdline(int argc, char** argv, std::vector& cmdline_applist) 1061 | { 1062 | static struct option long_opts[] = { 1063 | { "verbose", no_argument, 0, 0 }, 1064 | { "no-color", no_argument, 0, 0 }, 1065 | { "threads", required_argument, 0, 0 }, 1066 | { "ignoredir", required_argument, 0, 0 }, 1067 | { "path", required_argument, 0, 0 }, 1068 | { "short-stat", no_argument, 0, 0 }, 1069 | { "sort-by-instances", no_argument, 0, 0 }, 1070 | { "version", no_argument, 0, 0 }, 1071 | { "help", no_argument, 0, 0 }, 1072 | { 0, 0, 0, 0 } 1073 | }; 1074 | 1075 | // Let's pick the number of processors online (with a max of 32) for a default. 1076 | g_numthreads = std::min(g_numthreads, sysconf(_SC_NPROCESSORS_ONLN)); 1077 | 1078 | int c; 1079 | int opt_ind = 0; 1080 | while ((c = getopt_long(argc, argv, "p:?hvs", long_opts, &opt_ind)) != -1) { 1081 | switch (c) { 1082 | case 0: 1083 | if (!strcasecmp("help", long_opts[opt_ind].name)) { 1084 | print_usage(argv[0]); 1085 | exit(0); 1086 | }; 1087 | if (!strcasecmp("version", long_opts[opt_ind].name)) { 1088 | print_version(); 1089 | exit(0); 1090 | } 1091 | if (!strcasecmp("verbose", long_opts[opt_ind].name)) 1092 | g_verbose++; 1093 | else if (!strcasecmp("no-color", long_opts[opt_ind].name)) 1094 | set_no_color(); 1095 | else if (!strcasecmp("threads", long_opts[opt_ind].name)) 1096 | g_numthreads = atoi(optarg); 1097 | else if (!strcasecmp("ignoredir", long_opts[opt_ind].name)) { 1098 | std::string dirname = optarg; 1099 | if (dirname.size() > 1) { 1100 | if (optarg[dirname.size() - 1] != '/') 1101 | dirname += "/"; 1102 | ignore_dirs.push_back(dirname); 1103 | } 1104 | } else if (!strcasecmp("path", long_opts[opt_ind].name)) { 1105 | g_search_path = optarg; 1106 | } else if (!strcasecmp("short-stat", long_opts[opt_ind].name)) { 1107 | g_short_stat = 1; 1108 | } else if (!strcasecmp("sort-by-instances", long_opts[opt_ind].name)) { 1109 | g_sort_by_instances = 1; 1110 | } 1111 | break; 1112 | case 'p': 1113 | g_search_path = optarg; 1114 | break; 1115 | case 'v': 1116 | g_verbose++; 1117 | break; 1118 | case 's': 1119 | g_short_stat = 1; 1120 | break; 1121 | case 'h': 1122 | case '?': 1123 | print_usage(argv[0]); 1124 | exit(0); 1125 | default: 1126 | print_usage(argv[0]); 1127 | exit(-1); 1128 | break; 1129 | } 1130 | } 1131 | 1132 | for (; optind < argc; optind++) { 1133 | cmdline_applist.push_back(argv[optind]); 1134 | } 1135 | 1136 | parse_ignore_dirs_file(); 1137 | parse_search_dir(); 1138 | 1139 | if (g_verbose > 1) { 1140 | printf("%lu ignore_dirs:\n", ignore_dirs.size()); 1141 | 1142 | for (std::string& dname : ignore_dirs) { 1143 | printf(" '%s'\n", dname.c_str()); 1144 | } 1145 | } 1146 | } 1147 | 1148 | static void print_separator() 1149 | { 1150 | printf("%s%s%s\n", YELLOW, std::string(78, '-').c_str(), RESET); 1151 | } 1152 | 1153 | int main(int argc, char* argv[]) 1154 | { 1155 | std::vector cmdline_applist; 1156 | std::vector inotify_proclist; 1157 | 1158 | struct lconv* env = localeconv(); 1159 | if (env && env->thousands_sep && env->thousands_sep[0]) { 1160 | thousands_sep = env->thousands_sep[0]; 1161 | } 1162 | 1163 | parse_cmdline(argc, argv, cmdline_applist); 1164 | print_separator(); 1165 | 1166 | print_inotify_limits(); 1167 | print_separator(); 1168 | 1169 | if (init_inotify_proclist(inotify_proclist)) { 1170 | uint32_t total_watches = 0; 1171 | uint32_t total_instances = 0; 1172 | std::vector all_found_files; 1173 | 1174 | for (procinfo_t& procinfo : inotify_proclist) { 1175 | procinfo.in_cmd_line = is_proc_in_cmdline_applist(procinfo, cmdline_applist); 1176 | 1177 | total_watches += procinfo.watches; 1178 | total_instances += procinfo.instances; 1179 | } 1180 | 1181 | if (!g_short_stat) { 1182 | if (inotify_proclist.size()) { 1183 | print_inotify_proclist(inotify_proclist); 1184 | print_separator(); 1185 | } 1186 | } 1187 | 1188 | if (g_kernel_provides_watches_info) 1189 | printf("Total inotify Watches: %s%u%s\n", BGREEN, total_watches, RESET); 1190 | printf("Total inotify Instances: %s%u%s\n", BGREEN, total_instances, RESET); 1191 | print_separator(); 1192 | 1193 | if (!g_short_stat) { 1194 | double search_time = gettime(); 1195 | uint32_t total_scanned_dirs = find_files_in_inode_set(inotify_proclist, all_found_files); 1196 | if (total_scanned_dirs) { 1197 | search_time = gettime() - search_time; 1198 | 1199 | for (const filename_info_t& fname_info : all_found_files) { 1200 | printf("%s%9lu%s [%u:%u] %s\n", BGREEN, fname_info.inode, RESET, 1201 | major(fname_info.dev), minor(fname_info.dev), 1202 | fname_info.filename.c_str()); 1203 | } 1204 | 1205 | setlocale(LC_NUMERIC, ""); 1206 | GCC_DIAG_PUSH_OFF(format) 1207 | printf("\n%'u dirs scanned (%.2f seconds)\n", total_scanned_dirs, search_time); 1208 | GCC_DIAG_POP() 1209 | } 1210 | } 1211 | } 1212 | 1213 | return 0; 1214 | } 1215 | --------------------------------------------------------------------------------