Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ R CMD INSTALL .
```r
# Run tests (in the R console)
devtools::test()
# or without devtools:
# or without devtools (package must be installed and attached;
# NOT_CRAN=true also runs the tests that skip on aarch64):
library(watcher)
testthat::test_dir("tests/testthat")
```

Expand Down Expand Up @@ -69,6 +71,9 @@ The package uses GitHub Actions workflows in `.github/workflows/`:
**Callback Mechanism:**
- File events trigger `process_events()` callback in C
- Events are bundled by path and passed to R via the 'later' package's `execLaterNative2`
- Paths are deduplicated per callback (first occurrence kept) and copied in a
single allocation; combined with the patched-in event bubbling (see Build
System), paths reported by a single callback are always unique
- R callbacks execute when R is idle or when `later::run_now()` is called
- If no callback is provided, events are printed to stdout

Expand Down Expand Up @@ -100,10 +105,15 @@ hand-maintained config header, not host probing.
- `src/link.cpp`: empty `.cpp` that forces R to link with the C++ linker.
- `tools/update_libfswatch.sh`: re-vendors libfswatch and regenerates the config
header, object list, and Makevars; calls `tools/patch_libfswatch.sh`.
- `tools/patch_libfswatch.sh`: idempotent source patches — a null-format guard
in `string_utils`, self-guards on the fsevents/inotify/fanotify monitors, and
neutering of libfswatch's stdout/stderr/cerr logging (for the R CMD check
"compiled code" policy).
- `tools/patch_libfswatch.sh`: idempotent source patches — (1) empty
`string_utils.cpp` to an include-only TU: after the logging neutering its
only live references are two regex-error exception messages in `monitor.cpp`,
unreachable in watcher (no text filters are ever added), so those are
replaced with a static message; also fixes a GCC 16 `-Warray-bounds` false
positive. (2) Self-guards on the fsevents/inotify/fanotify monitors. (3)
Neutering of libfswatch's stdout/stderr/cerr logging (for the R CMD check
"compiled code" policy). (4) `bubble_events = true` by default (the C API
does not expose `set_bubble_events()`).

**Key dependencies:** an R C/C++ toolchain plus `make`; pthread; the
'later' R package. A system libfswatch is used automatically when present.
Expand Down
8 changes: 2 additions & 6 deletions src/fswatch/libfswatch/src/libfswatch/c++/monitor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,7 @@ namespace fsw
catch (const std::regex_error& error)
{
throw libfsw_exception(
string_utils::string_from_format(
_("An error occurred during the compilation of %s"),
filter.text.c_str()),
_("An error occurred during the compilation of a filter regex"),
FSW_ERR_INVALID_REGEX);
}
}
Expand All @@ -148,9 +146,7 @@ namespace fsw
catch (const std::regex_error& error)
{
throw libfsw_exception(
string_utils::string_from_format(
_("An error occurred during the compilation of %s"),
filter.text.c_str()),
_("An error occurred during the compilation of a filter regex"),
FSW_ERR_INVALID_REGEX);
}
}
Expand Down
54 changes: 4 additions & 50 deletions src/fswatch/libfswatch/src/libfswatch/c++/string/string_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,54 +13,8 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <cstdarg>
#include <cstdio>
#include <vector>
/* watcher: bodies removed as dead code. Logging is neutered, and the only
* other callers (regex-error paths in monitor.cpp) are unreachable in watcher.
* Also sidesteps a GCC 16 -Warray-bounds false positive.
* See tools/patch_libfswatch.sh. */
#include "string_utils.hpp"

using namespace std;

namespace fsw
{
namespace string_utils
{
string vstring_from_format(const char *format, va_list args)
{
if (!format) return string();
size_t current_buffer_size = 0;
int required_chars = 512;

vector<char> buffer;

do
{
current_buffer_size += required_chars;
buffer.resize(current_buffer_size);
required_chars = vsnprintf(&buffer[0], current_buffer_size, format, args);

// If an encoding error occurs, break and write an empty string into the
// buffer.
if (required_chars < 0)
{
buffer.resize(1);
break;
}
}
while ((size_t) required_chars > current_buffer_size);

return string(&buffer[0]);
}

string string_from_format(const char *format, ...)
{
va_list args;
va_start(args, format);

string ret = vstring_from_format(format, args);

va_end(args);

return ret;
}
}
}
60 changes: 47 additions & 13 deletions tools/patch_libfswatch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,14 @@
#
# Every patch is IDEMPOTENT: running this on already-patched sources is a no-op.
# The patches are:
# 1. string_utils.cpp - guard against a null format string (GCC
# -Wformat-truncation in stricter CRAN/rhub builds).
# 1. string_utils.cpp body removal - after patch 3's logging neutering,
# string_from_format()/vstring_from_format() are referenced only by two
# regex-error exception messages in monitor.cpp, which watcher can never
# reach (it adds only event-flag filters, never text filters). Replace
# those call sites with a static message (1a) and empty string_utils.cpp
# to an include-only TU (1b). Also resolves a GCC 16 -Warray-bounds false
# positive against the upstream vector<char>::resize implementation, which
# R CMD check reports as a significant warning.
# 2. Monitor self-guards - wrap fsevents/inotify/fanotify monitor bodies in
# their platform #ifdef so they compile to empty objects on the wrong OS
# (kqueue/fen/windows monitors already self-guard upstream).
Expand Down Expand Up @@ -59,19 +65,47 @@ add_guard() {
echo -e "${GREEN} ✓ self-guard $macro applied${NC}"
}

# --- Patch 1: string_utils.cpp null-format guard -----------------------------
STRING_UTILS="$LIBFSW_SRC/c++/string/string_utils.cpp"
if [ -f "$STRING_UTILS" ]; then
if grep -q "if (!format) return" "$STRING_UTILS"; then
echo -e "${GREEN} ✓ string_utils null-format guard already present${NC}"
else
sed -i.bak 's|^ size_t current_buffer_size = 0;$| if (!format) return string();\
size_t current_buffer_size = 0;|' "$STRING_UTILS"
rm -f "$STRING_UTILS.bak"
echo -e "${GREEN} ✓ string_utils null-format guard applied${NC}"
# --- Patch 1a: static regex-error messages in monitor.cpp --------------------
# watcher never adds text filters (only event-flag filters), so the regex-error
# exceptions in add_filter()/add_prune_filter() are unreachable. Replace their
# string_from_format() messages with a static string so string_utils.cpp can be
# emptied (patch 1b).
MON_CPP="$LIBFSW_SRC/c++/monitor.cpp"
if grep -q 'compilation of a filter regex' "$MON_CPP" 2>/dev/null; then
echo -e "${GREEN} ✓ monitor.cpp regex-error messages already static${NC}"
else
perl -0777 -pi -e 's/ string_utils::string_from_format\(\n _\("An error occurred during the compilation of %s"\),\n filter\.text\.c_str\(\)\),/ _("An error occurred during the compilation of a filter regex"),/g' "$MON_CPP"
if grep -q 'filter\.text\.c_str()' "$MON_CPP"; then
echo "Error: failed to patch string_from_format call sites in $MON_CPP" >&2
exit 1
fi
echo -e "${GREEN} ✓ monitor.cpp regex-error messages made static${NC}"
fi

# --- Patch 1b: empty string_utils.cpp bodies (dead code) ---------------------
# After patch 3's logging neutering and patch 1a, nothing references
# string_from_format()/vstring_from_format(): empty the TU. The object stays in
# the build, compiling empty like the self-guarded monitors. This also resolves
# the GCC 16 -Warray-bounds false positive against the upstream
# vector<char>::resize implementation, which R CMD check reports as a
# significant warning.
STRING_UTILS="$LIBFSW_SRC/c++/string/string_utils.cpp"
if grep -q 'watcher: bodies removed' "$STRING_UTILS" 2>/dev/null; then
echo -e "${GREEN} ✓ string_utils.cpp bodies already removed${NC}"
else
echo -e "${YELLOW} ⚠ string_utils.cpp not found, skipping${NC}"
# Preserve the upstream licence header (everything before the first #include).
HEADER=$(awk '/^#include/{exit} {print}' "$STRING_UTILS")
{
printf '%s\n' "$HEADER"
cat <<'EOF'
/* watcher: bodies removed as dead code. Logging is neutered, and the only
* other callers (regex-error paths in monitor.cpp) are unreachable in watcher.
* Also sidesteps a GCC 16 -Warray-bounds false positive.
* See tools/patch_libfswatch.sh. */
#include "string_utils.hpp"
EOF
} > "$STRING_UTILS.tmp" && mv "$STRING_UTILS.tmp" "$STRING_UTILS"
echo -e "${GREEN} ✓ string_utils.cpp bodies removed${NC}"
fi

# --- Patch 2: self-guard the three non-self-guarding monitors ----------------
Expand Down
Loading