diff --git a/.github/workflows/macos-diag.yml b/.github/workflows/macos-diag.yml new file mode 100644 index 00000000..cad62c24 --- /dev/null +++ b/.github/workflows/macos-diag.yml @@ -0,0 +1,61 @@ +name: macos-diag + +# Manual diagnostic only: builds the unit tests on macOS, runs them with +# per-assertion output and, if they stall, dumps thread stacks. +on: + workflow_dispatch: + +jobs: + diag: + runs-on: macos-14 + timeout-minutes: 60 + steps: + - uses: actions/checkout@v5 + with: + fetch-tags: true + fetch-depth: 0 + - uses: seanmiddleditch/gha-setup-ninja@v6 + - name: Configure + run: cmake --preset default + - name: Build + run: cmake --build build/default --config Release --target openads_unit_tests + - name: Run unit tests with stall dump + shell: bash + run: | + BIN=$(find build -name openads_unit_tests -type f -perm -u+x | head -1) + echo "binary: $BIN" + cd "$(dirname "$BIN")" + script -q "$GITHUB_WORKSPACE/run.log" ./openads_unit_tests -tce='*[slow]*,*flaky*' -s --no-colors > /dev/null 2>&1 & + SP=$! + for i in $(seq 1 56); do + sleep 10 + if ! kill -0 $SP 2>/dev/null; then echo "finished early at ~$((i*10))s"; break; fi + done + if kill -0 $SP 2>/dev/null; then + echo "=== STILL RUNNING after ~560s: stall dump" + PID=$(pgrep -f './openads_unit_tests' | grep -v "^$SP$" | tail -1) + echo "test pid: $PID" + ps -o pid,stat,%cpu,etime,command -p "$PID" || true + echo "=== last 40 lines of output" + tail -n 40 "$GITHUB_WORKSPACE/run.log" | tr -d '\r' | cat -v | cut -c1-200 || true + echo "=== lock timeouts in output" + tr -d '\r' < "$GITHUB_WORKSPACE/run.log" | grep -c 'byte lock wait' || true + tr -d '\r' < "$GITHUB_WORKSPACE/run.log" | grep -n -B6 'byte lock wait' | cut -c1-200 | head -40 || true + echo "=== tests started" + tr -d '\r' < "$GITHUB_WORKSPACE/run.log" | grep -c '^TEST CASE' || true + echo "=== lsof (tmp files)" + lsof -p "$PID" 2>/dev/null | grep -i openads | cut -c1-200 | head -40 || true + echo "=== all processes" + ps -axo pid,ppid,stat,etime,command | grep -i openads | grep -v grep | cut -c1-200 || true + echo "=== lldb backtraces" + sudo lldb -p "$PID" -b -o 'thread backtrace all' > "$GITHUB_WORKSPACE/lldb.txt" 2>&1 || true + tr -d '\r' < "$GITHUB_WORKSPACE/lldb.txt" | cat -v | cut -c1-220 | head -120 + echo "=== sample" + sudo sample "$PID" 5 -file "$GITHUB_WORKSPACE/sample.txt" > /dev/null 2>&1 || true + tr -d '\r' < "$GITHUB_WORKSPACE/sample.txt" | cat -v | cut -c1-200 | head -100 + echo "=== end of dump" + kill -9 "$PID" || true + else + echo "=== finished; tail of output" + tail -n 30 "$GITHUB_WORKSPACE/run.log" | tr -d '\r' | cat -v | cut -c1-200 || true + fi diff --git a/release-notes-1.09.71.md b/release-notes-1.09.71.md index 2dfca873..80133502 100644 --- a/release-notes-1.09.71.md +++ b/release-notes-1.09.71.md @@ -28,6 +28,7 @@ Reported by Tim (via Antonio) while converting an application's data to `.adt`. * `openads-1.09.71-windows-x64.zip` * `openads-1.09.71-windows-x86.zip` * `openads-1.09.71-linux-x64.tar.gz` -* macOS: NOT included in this release / sin binario macOS esta vez - (macOS CI stalls in the unit tests; under investigation). +* `openads-1.09.71-macos-universal.tar.gz` - built, but its unit tests + stall in CI and did not complete, so this package is UNTESTED / + paquete macOS sin pruebas completas (under investigation). `release.yml` treats the macOS leg as optional until that is fixed. diff --git a/src/platform/lock_posix.cpp b/src/platform/lock_posix.cpp index e7d42bbe..579070e0 100644 --- a/src/platform/lock_posix.cpp +++ b/src/platform/lock_posix.cpp @@ -5,7 +5,9 @@ #include #include #include +#include #include +#include #include namespace openads::platform { @@ -28,10 +30,10 @@ util::Error os_error(const char* op) { // semantics used by the engine. #ifdef F_OFD_SETLK constexpr int kSetLk = F_OFD_SETLK; -constexpr int kSetLkW = F_OFD_SETLKW; +[[maybe_unused]] constexpr int kSetLkW = F_OFD_SETLKW; #else constexpr int kSetLk = F_SETLK; -constexpr int kSetLkW = F_SETLKW; +[[maybe_unused]] constexpr int kSetLkW = F_SETLKW; #endif // off_t is signed: lock offsets at or above 2^63 (the ADT lock base is @@ -101,7 +103,51 @@ void ByteLock::release_() noexcept { util::Result ByteLock::acquire(File& f, std::uint64_t offset, std::uint64_t length, LockKind kind) { +#if defined(F_OFD_SETLK) && !defined(__APPLE__) return do_lock(f, offset, length, kind, kSetLkW); +#else + // macOS: its SDK defines the OFD commands, but a blocking F_OFD_SETLKW + // waits forever when a leaked holder lives in this same process (CI + // test runs sat on it until the job timeout). Poll the non-blocking + // command and give up after a bounded wait, so a stall becomes a lock + // error instead of a hang. + const auto deadline = + std::chrono::steady_clock::now() + std::chrono::seconds(30); + for (;;) { + auto r = do_lock(f, offset, length, kind, kSetLk); + if (r) return r; + if (r.error().sub_code != EAGAIN && r.error().sub_code != EACCES) { + return r; + } + if (std::chrono::steady_clock::now() >= deadline) { + struct flock q{}; + q.l_type = (kind == LockKind::Exclusive) ? F_WRLCK : F_RDLCK; + q.l_whence = SEEK_SET; + q.l_start = static_cast(fold_lock_offset(offset)); + q.l_len = static_cast(length); + int fd = static_cast( + reinterpret_cast(f.native_handle()) - 1); + long holder = -1; +#ifdef F_OFD_GETLK + q.l_pid = 0; + const int kGetLk = F_OFD_GETLK; +#else + const int kGetLk = F_GETLK; +#endif + if (::fcntl(fd, kGetLk, &q) == 0 && q.l_type != F_UNLCK) { + holder = static_cast(q.l_pid); + } + std::fprintf(stderr, + "openads: byte lock wait timed out (offset=%llu len=%llu " + "holder_pid=%ld self_pid=%ld)\n", + static_cast(offset), + static_cast(length), holder, + static_cast(::getpid())); + return r; + } + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + } +#endif } util::Result ByteLock::try_acquire(File& f, std::uint64_t offset,