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
4 changes: 2 additions & 2 deletions .okf/api/multi.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Static 1:1 bindings for libcurl multi calls ext-curl leaves out
resource: /pcurl/multi.zep
tags: [pcurl, api, libcurl]
status: draft
generated: { by: claude-code/claude-opus-5.5, at: "2026-10-11T12:00:00Z" }
generated: { by: cursor-agent/grok-4.6, at: "2026-10-14T13:30:00Z" }
sources:
- id: zep
resource: /pcurl/multi.zep
Expand Down Expand Up @@ -41,7 +41,7 @@ Every method is named after the C call it binds and takes ext-curl's `CurlMultiH

* Long options (`< CURLOPTTYPE_OBJECTPOINT`) go straight to `curl_multi_setopt`.
* `CURLMOPT_SOCKETFUNCTION` (20001): `callable(?CurlHandle $easy, int $s, int $what, mixed $clientp, mixed $socketp): int`. `$easy` is the ext-curl object found in the multi's handle list, or `null`.
* `CURLMOPT_TIMERFUNCTION` (20004): `callable(CurlMultiHandle $multi, int $timeoutMs, mixed $clientp): int`.
* `CURLMOPT_TIMERFUNCTION` (20004): `callable(CurlMultiHandle $multi, int $timeoutMs, mixed $clientp): int`. libcurl invokes it when the timeout it wants changes, not on every `curl_multi_socket_action(CURL_SOCKET_TIMEOUT)`. On 8.5 that TIMEOUT action clears `timer_lastcall` then skips the callback if the new timeout is `-1`; 8.11+ keeps `last_timeout_ms` and still reports `-1`. The suite's timer tests set `CURLOPT_TIMEOUT_MS` so both trees have an expire to report.
* `CURLMOPT_SOCKETDATA` (10002) / `CURLMOPT_TIMERDATA` (10005): any value, passed back as `$clientp`. libcurl's own data pointer is always pcurl's state, never the PHP value.
* `null` uninstalls a function; a non-callable returns `CURLM_BAD_FUNCTION_ARGUMENT` (10) and leaves the old one in place.
* Any other object/function option → `CURLM_UNKNOWN_OPTION` (6). `CURLMOPT_PUSHFUNCTION` stays with ext-curl's `curl_multi_setopt`.
Expand Down
4 changes: 4 additions & 0 deletions .okf/log.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Directory Update Log

## 2026-10-14

* **TimerCallbackTest vs libcurl 8.5**: Ubuntu 24.04 CI (libcurl 8.5.0) failed 4 tests that assumed `curlMultiSocketAction(CURL_SOCKET_TIMEOUT)` always invokes `CURLMOPT_TIMERFUNCTION`. It doesn't: 8.5's `multi_socket` memsets `timer_lastcall` to `{0,0}` on `CURL_SOCKET_TIMEOUT`, then `Curl_update_timer` skips the callback when the new timeout is `-1` (already "none"). A localhost transfer with no `CURLOPT_TIMEOUT` looks exactly like that after `EXPIRE_RUN_NOW` is consumed — add_handle still fires `0`, `curl_multi_exec` still fires, completion never sends `-1`. 8.11 (`a58b50f`) split that bookkeeping into `last_expire_ts` (cleared) and `last_timeout_ms` (kept), so the same TIMEOUT call reports `-1`; Homebrew 8.22 and the Pi's 8.14.1 follow that path and were green. The tests now put `CURLOPT_TIMEOUT_MS` on the easy so 8.5 has an expire left to report. C unchanged: the trampoline was firing whenever libcurl asked.

## 2026-10-12

* **Version**: 0.10.5 in `config.json`, `composer.json`, `PHP_PCURL_VERSION`. No API change; `ide/0.10.5/` generated with `zephir stubs`, identical to `ide/0.10.1/`. Steps past the 0.10.3 and 0.10.4 numbers to line up with the sibling extensions.
Expand Down
26 changes: 21 additions & 5 deletions tests/TimerCallbackTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

it('is asked to cancel with -1 once the transfer completes', function (): void {
$timeouts = [];
$easy = easyFor('/cancel');
$easy = easyWithTimeout('/cancel');
drive($this->multi, [$easy], null, function (CurlMultiHandle $m, int $timeoutMs) use (&$timeouts): void {
$timeouts[] = $timeoutMs;
});
Expand All @@ -38,14 +38,30 @@
});

/*
* libcurl calls the timer callback only when the timeout it wants changes. Each test below installs a
* no-op callback before adding the handle, so libcurl has a recorded timeout (0) that the next call
* moves, then swaps in the callback under test.
* libcurl calls TIMERFUNCTION only when the timeout it wants changes.
*
* curl_multi_socket_action(CURL_SOCKET_TIMEOUT) on libcurl 8.5 (Ubuntu 24.04) memsets
* timer_lastcall to {0,0} first, then skips the callback if the new timeout is -1 — which
* is what a localhost transfer looks like once RUN_NOW has been consumed and nothing else
* is in the expire tree. 8.11+ keeps last_timeout_ms across that memset, so the same call
* reports -1 even without an easy timeout.
*
* CURLOPT_TIMEOUT_MS leaves an expire in the tree: TIMEOUT then reports the remaining ms,
* and completion reports -1. Each test below installs a no-op callback before adding that
* easy, so libcurl records 0, then swaps in the callback under test for the next change.
*/
function easyWithTimeout(string $path): CurlHandle
{
$easy = easyFor($path);
curl_setopt($easy, CURLOPT_TIMEOUT_MS, 5_000);

return $easy;
}

function multiWithPendingTransfer(CurlMultiHandle $multi, string $path): void
{
Multi::curlMultiSetopt($multi, CURLMOPT_TIMERFUNCTION, fn (): int => 0);
curl_multi_add_handle($multi, easyFor($path));
curl_multi_add_handle($multi, easyWithTimeout($path));
}

it('treats a null return as 0, so libcurl carries on', function (): void {
Expand Down
Loading