LCOV - code coverage report
Current view: top level - src - excimer_timer.c (source / functions) Hit Total Coverage
Test: mediawiki/php/excimer test coverage report Lines: 102 128 79.7 %
Date: 2025-08-22 19:33:56 Functions: 15 15 100.0 %

          Line data    Source code
       1             : /* Copyright 2018 Wikimedia Foundation
       2             :  *
       3             :  * Licensed under the Apache License, Version 2.0 (the "License");
       4             :  * you may not use this file except in compliance with the License.
       5             :  * You may obtain a copy of the License at
       6             :  *
       7             :  *     http://www.apache.org/licenses/LICENSE-2.0
       8             :  *
       9             :  * Unless required by applicable law or agreed to in writing, software
      10             :  * distributed under the License is distributed on an "AS IS" BASIS,
      11             :  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             :  * See the License for the specific language governing permissions and
      13             :  * limitations under the License.
      14             :  */
      15             : 
      16             : #include <signal.h>
      17             : #include <stdint.h>
      18             : #include <time.h>
      19             : #include <pthread.h>
      20             : #include <stdio.h>
      21             : #include <stdlib.h>
      22             : 
      23             : #include "php.h"
      24             : #include "excimer_mutex.h"
      25             : #include "excimer_timer.h"
      26             : #include "zend_types.h"
      27             : 
      28             : #if PHP_VERSION_ID >= 80200
      29             : #define excimer_timer_atomic_bool_store(dest, value) zend_atomic_bool_store(dest, value)
      30             : #else
      31             : #define excimer_timer_atomic_bool_store(dest, value) *dest = value
      32             : #endif
      33             : 
      34             : excimer_timer_globals_t excimer_timer_globals;
      35             : ZEND_TLS excimer_timer_tls_t excimer_timer_tls;
      36             : 
      37             : static void excimer_timer_handle(void * data, int overrun_count);
      38             : static void excimer_timer_interrupt(zend_execute_data *execute_data);
      39             : 
      40             : /**
      41             :  * Add a timer to the pending list. Unsynchronised, i.e. the caller is
      42             :  * responsible for locking the mutex if required.
      43             :  */
      44          90 : static void excimer_timer_list_enqueue(excimer_timer *timer)
      45             : {
      46          90 :     excimer_timer **head_pp = &excimer_timer_tls.pending_head;
      47          90 :     if (!timer->pending_next) {
      48          90 :         if (*head_pp) {
      49           0 :             timer->pending_next = *head_pp;
      50           0 :             timer->pending_prev = (*head_pp)->pending_prev;
      51           0 :             (*head_pp)->pending_prev->pending_next = timer;
      52           0 :             (*head_pp)->pending_prev = timer;
      53             :         } else {
      54          90 :             *head_pp = timer;
      55          90 :             timer->pending_next = timer;
      56          90 :             timer->pending_prev = timer;
      57             :         }
      58             :     }
      59          90 : }
      60             : 
      61             : /**
      62             :  * Remove the first (FIFO) timer from the pending list and provide a pointer
      63             :  * to it. (unsynchronised)
      64             :  *
      65             :  * @param[out] timer_pp
      66             :  * @return True if a timer was returned, false if the list was empty
      67             :  */
      68         180 : static int excimer_timer_list_dequeue(excimer_timer **timer_pp)
      69             : {
      70         180 :     excimer_timer **head_pp = &excimer_timer_tls.pending_head;
      71         180 :     if (*head_pp) {
      72             :         // Get the pending timer
      73          90 :         excimer_timer *timer = *timer_pp = *head_pp;
      74          90 :         if (timer->pending_next == timer) {
      75             :             // List is now empty
      76          90 :             *head_pp = NULL;
      77             :         } else {
      78             :             // Relink the head and neighbours
      79           0 :             timer->pending_next->pending_prev = timer->pending_prev;
      80           0 :             *head_pp = timer->pending_prev->pending_next = timer->pending_next;
      81             :         }
      82             :         // Unlink the timer being returned
      83          90 :         timer->pending_next = NULL;
      84          90 :         timer->pending_prev = NULL;
      85          90 :         return 1;
      86             :     } else {
      87          90 :         return 0;
      88             :     }
      89             : }
      90             : 
      91             : /**
      92             :  * Remove the specified timer from the pending list, if it is in there. If it
      93             :  * is not in the list, do nothing. (unsynchronised)
      94             :  */
      95          41 : static void excimer_timer_list_remove(excimer_timer *timer)
      96             : {
      97          41 :     excimer_timer **head_pp = &excimer_timer_tls.pending_head;
      98          41 :     if (timer->pending_next) {
      99           0 :         if (timer->pending_next == timer) {
     100           0 :             *head_pp = NULL;
     101             :         } else {
     102           0 :             timer->pending_next->pending_prev = timer->pending_prev;
     103           0 :             timer->pending_prev->pending_next = timer->pending_next;
     104           0 :             if (*head_pp == timer) {
     105           0 :                 *head_pp = timer->pending_next;
     106             :             }
     107             :         }
     108           0 :         timer->pending_next = NULL;
     109           0 :         timer->pending_prev = NULL;
     110             :     }
     111          41 : }
     112             : 
     113             : /**
     114             :  * Atomically dequeue a timer and get its event count at the time of removal
     115             :  * from the queue. The timer may be immediately re-added to the queue by the
     116             :  * event handler.
     117             :  *
     118             :  * @param[out] timer_pp Where to put the pointer to the timer
     119             :  * @param[out] event_count_p Where to put the event count
     120             :  * @return True if a timer was removed, false if the list was empty.
     121             :  */
     122         180 : static int excimer_timer_pending_dequeue(excimer_timer **timer_pp, zend_long *event_count_p)
     123             : {
     124         180 :     excimer_mutex_lock(&excimer_timer_tls.mutex);
     125         180 :     int ret = excimer_timer_list_dequeue(timer_pp);
     126         180 :     if (ret) {
     127          90 :         *event_count_p = (*timer_pp)->event_count;
     128          90 :         (*timer_pp)->event_count = 0;
     129             :     }
     130         180 :     excimer_mutex_unlock(&excimer_timer_tls.mutex);
     131         180 :     return ret;
     132             : }
     133             : 
     134             : // Note: functions with external linkage are documented in the header
     135             : 
     136          17 : void excimer_timer_module_init()
     137             : {
     138          17 :     excimer_timer_globals.old_zend_interrupt_function = zend_interrupt_function;
     139          17 :     zend_interrupt_function = excimer_timer_interrupt;
     140          17 : }
     141             : 
     142          17 : void excimer_timer_module_shutdown()
     143             : {
     144          17 : }
     145             : 
     146          17 : void excimer_timer_thread_init()
     147             : {
     148          17 :     excimer_timer_tls = (excimer_timer_tls_t){
     149             :         .mutex = PTHREAD_MUTEX_INITIALIZER
     150             :     };
     151          17 : }
     152             : 
     153          17 : void excimer_timer_thread_shutdown()
     154             : {
     155          17 :     if (excimer_timer_tls.timers_active) {
     156             :         // If this ever happens, it means we've got the logic wrong and we need
     157             :         // to rethink. It's very bad for timers to keep existing after thread
     158             :         // termination, because the mutex will be a dangling pointer. It's not
     159             :         // much help to avoid excimer_mutex_destroy() here because the whole TLS
     160             :         // segment will be destroyed and reused.
     161           0 :         php_error_docref(NULL, E_WARNING, "Timer still active at thread termination");
     162             :     } else {
     163          17 :         excimer_mutex_destroy(&excimer_timer_tls.mutex);
     164             :     }
     165          17 : }
     166             : 
     167          41 : int excimer_timer_init(excimer_timer *timer, int event_type,
     168             :     excimer_timer_callback callback, void *user_data)
     169             : {
     170             :     zval z_timer;
     171             : 
     172          41 :     memset(timer, 0, sizeof(excimer_timer));
     173          41 :     ZVAL_PTR(&z_timer, timer);
     174          41 :     timer->vm_interrupt_ptr = &EG(vm_interrupt);
     175          41 :     timer->callback = callback;
     176          41 :     timer->user_data = user_data;
     177          41 :     timer->tls = &excimer_timer_tls;
     178             : 
     179          41 :     if (timerlib_timer_init(&timer->tl_timer, event_type, &excimer_timer_handle, timer) == FAILURE) {
     180           0 :         timerlib_timer_destroy(&timer->tl_timer);
     181           0 :         return FAILURE;
     182             :     }
     183             : 
     184          41 :     excimer_timer_tls.timers_active++;
     185          41 :     timer->is_valid = 1;
     186          41 :     timer->is_running = 0;
     187          41 :     return SUCCESS;
     188             : }
     189             : 
     190          43 : void excimer_timer_start(excimer_timer *timer,
     191             :     struct timespec *period, struct timespec *initial)
     192             : {
     193          43 :     if (!timer->is_valid) {
     194           0 :         php_error_docref(NULL, E_WARNING, "Unable to start uninitialised timer" );
     195           0 :         return;
     196             :     }
     197             : 
     198             :     /* If a periodic timer has an initial value of 0, use the period instead,
     199             :      * since it_value=0 means disarmed */
     200          43 :     if (timerlib_timespec_is_zero(initial)) {
     201          12 :         initial = period;
     202             :     }
     203             :     /* If the value is still zero, flag an error */
     204          43 :     if (timerlib_timespec_is_zero(initial)) {
     205           0 :         php_error_docref(NULL, E_WARNING, "Unable to start timer with a value of zero "
     206             :             "duration and period");
     207           0 :         return;
     208             :     }
     209             : 
     210          43 :     if (timerlib_timer_start(&timer->tl_timer, period, initial) == SUCCESS) {
     211          43 :         timer->is_running = 1;
     212             :     }
     213             : }
     214             : 
     215           7 : void excimer_timer_stop(excimer_timer *timer)
     216             : {
     217           7 :     if (!timer->is_valid) {
     218           0 :         php_error_docref(NULL, E_WARNING, "Unable to start uninitialised timer" );
     219           0 :         return;
     220             :     }
     221           7 :     if (timer->is_running) {
     222           7 :         if (timerlib_timer_stop(&timer->tl_timer) == SUCCESS) {
     223           7 :             timer->is_running = 0;
     224             :         }
     225             :     }
     226             : }
     227             : 
     228          41 : void excimer_timer_destroy(excimer_timer *timer)
     229             : {
     230          41 :     if (!timer->is_valid) {
     231             :         /* This could happen if the timer is manually destroyed after
     232             :          * excimer_timer_thread_shutdown() is called */
     233           0 :         return;
     234             :     }
     235          41 :     if (timer->tls != &excimer_timer_tls) {
     236           0 :         php_error_docref(NULL, E_WARNING,
     237             :             "Cannot delete a timer belonging to a different thread");
     238           0 :         return;
     239             :     }
     240             : 
     241             :     /* Stop the timer */
     242          41 :     if (timer->is_running) {
     243          36 :         timer->is_running = 0;
     244          36 :         timerlib_timer_stop(&timer->tl_timer);
     245             :     }
     246             :     /* Destroy the timer. This will wait until any events are done. */
     247          41 :     timerlib_timer_destroy(&timer->tl_timer);
     248          41 :     excimer_timer_tls.timers_active--;
     249             : 
     250             :     /* Remove the timer from the pending list */
     251          41 :     excimer_mutex_lock(&excimer_timer_tls.mutex);
     252          41 :     excimer_timer_list_remove(timer);
     253          41 :     excimer_mutex_unlock(&excimer_timer_tls.mutex);
     254             : 
     255          41 :     timer->is_valid = 0;
     256          41 :     timer->tls = NULL;
     257             : }
     258             : 
     259          90 : static void excimer_timer_handle(void * data, int overrun_count)
     260             : {
     261          90 :     excimer_timer *timer = (excimer_timer*)data;
     262          90 :     excimer_mutex_lock(&excimer_timer_tls.mutex);
     263          90 :     timer->event_count += overrun_count + 1;
     264          90 :     excimer_timer_list_enqueue(timer);
     265          90 :     excimer_mutex_unlock(&excimer_timer_tls.mutex);
     266          90 :     excimer_timer_atomic_bool_store(timer->vm_interrupt_ptr, 1);
     267          90 : }
     268             : 
     269          90 : static void excimer_timer_interrupt(zend_execute_data *execute_data)
     270             : {
     271          90 :     excimer_timer *timer = NULL;
     272          90 :     zend_long count = 0;
     273         180 :     while (excimer_timer_pending_dequeue(&timer, &count)) {
     274          90 :         timer->callback(count, timer->user_data);
     275             :     }
     276             : 
     277          90 :     if (excimer_timer_globals.old_zend_interrupt_function) {
     278          90 :         excimer_timer_globals.old_zend_interrupt_function(execute_data);
     279             :     }
     280          90 : }
     281             : 
     282           3 : void excimer_timer_get_time(excimer_timer *timer, struct timespec *remaining)
     283             : {
     284           3 :     if (!timer->is_valid || !timer->is_running) {
     285           2 :         remaining->tv_sec = 0;
     286           2 :         remaining->tv_nsec = 0;
     287           2 :         return;
     288             :     }
     289             : 
     290           1 :     timerlib_timer_get_time(&timer->tl_timer, remaining);
     291             : }

Generated by: LCOV version 1.14