]> granicus.if.org Git - php/commitdiff
Add support for replaying warnings in opcache
authorNikita Popov <nikita.ppv@gmail.com>
Tue, 19 May 2020 13:01:18 +0000 (15:01 +0200)
committerNikita Popov <nikita.ppv@gmail.com>
Wed, 20 May 2020 09:20:21 +0000 (11:20 +0200)
If opcache.record_warnings is enabled, opcache will record
compilation warnings and replay them when the file is included
again. The primary use case I have in mind for this is automated
testing of the opcache file cache.

This resolves bug #76535.

12 files changed:
NEWS
UPGRADING
ext/opcache/ZendAccelerator.c
ext/opcache/ZendAccelerator.h
ext/opcache/tests/warning_replay.inc [new file with mode: 0644]
ext/opcache/tests/warning_replay.phpt [new file with mode: 0644]
ext/opcache/zend_accelerator_module.c
ext/opcache/zend_file_cache.c
ext/opcache/zend_persist.c
ext/opcache/zend_persist_calc.c
php.ini-development
php.ini-production

diff --git a/NEWS b/NEWS
index 7d42d73cfc0d62083c6a29cb78fc1e43259120fe..e27c38689cb4ae4aab7fa5b0068d29fa292a5032 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -90,6 +90,7 @@ PHP                                                                        NEWS
 - OpCache:
   . Fixed bug #78654 (Incorrectly computed opcache checksum on files with
     non-ascii characters). (mhagstrand)
+  . Fixed bug #76535 (Opcache does not replay compile-time warnings). (Nikita)
 
 - PCRE:
   . Don't ignore invalid escape sequences. (sjon)
index 05929c548b6ee35fe7e5182b407496d1417433a0..a3c471c3194cd01bc20dee05d71987ad7ba31a9b 100644 (file)
--- a/UPGRADING
+++ b/UPGRADING
@@ -517,6 +517,11 @@ PHP 8.0 UPGRADE NOTES
   . Introduce DOMParentNode and DOMChildNode with new traversal and manipulation APIs
     RFC: https://wiki.php.net/rfc/dom_living_standard_api
 
+- Opcache:
+  . If the opcache.record_warnings ini setting is enabled, opcache will record
+    compile-time warnings and replay them on the next include, even if it is
+    served from cache.
+
 - Zip:
   . Extension updated to version 1.19.0
   . New ZipArchive::lastId property to get index value of last added entry.
index 2b858bc228767611099e4babe27f038dac6e6eb5..79941a7d13d3941a57b70de3d58abe3e25e9becd 100644 (file)
@@ -121,6 +121,7 @@ zend_bool fallback_process = 0; /* process uses file cache fallback */
 static zend_op_array *(*accelerator_orig_compile_file)(zend_file_handle *file_handle, int type);
 static int (*accelerator_orig_zend_stream_open_function)(const char *filename, zend_file_handle *handle );
 static zend_string *(*accelerator_orig_zend_resolve_path)(const char *filename, size_t filename_len);
+static void (*accelerator_orig_zend_error_cb)(int type, const char *error_filename, const uint32_t error_lineno, const char *format, va_list args);
 static zif_handler orig_chdir = NULL;
 static ZEND_INI_MH((*orig_include_path_on_modify)) = NULL;
 static int (*orig_post_startup_cb)(void);
@@ -1661,6 +1662,56 @@ static void zend_accel_init_auto_globals(void)
        }
 }
 
+static void persistent_error_cb(int type, const char *error_filename, const uint32_t error_lineno, const char *format, va_list args) {
+       if (ZCG(record_warnings)) {
+               zend_recorded_warning *warning = emalloc(sizeof(zend_recorded_warning));
+               va_list args_copy;
+               warning->type = type;
+               warning->error_lineno = error_lineno;
+               warning->error_filename = zend_string_init(error_filename, strlen(error_filename), 0);
+               va_copy(args_copy, args);
+               warning->error_message = zend_vstrpprintf(0, format, args_copy);
+               va_end(args_copy);
+
+               ZCG(num_warnings)++;
+               ZCG(warnings) = erealloc(ZCG(warnings), sizeof(zend_recorded_warning) * ZCG(num_warnings));
+               ZCG(warnings)[ZCG(num_warnings)-1] = warning;
+       }
+       accelerator_orig_zend_error_cb(type, error_filename, error_lineno, format, args);
+}
+
+/* Hack to get us a va_list to pass to zend_error_cb. */
+static void replay_warning_helper(const zend_recorded_warning *warning, ...) {
+       va_list va;
+       va_start(va, warning);
+       accelerator_orig_zend_error_cb(
+               warning->type, ZSTR_VAL(warning->error_filename), warning->error_lineno, "%s", va);
+       va_end(va);
+}
+
+static void replay_warnings(zend_persistent_script *script) {
+       for (uint32_t i = 0; i < script->num_warnings; i++) {
+               zend_recorded_warning *warning = script->warnings[i];
+               replay_warning_helper(warning, ZSTR_VAL(warning->error_message));
+       }
+}
+
+static void free_recorded_warnings() {
+       if (!ZCG(num_warnings)) {
+               return;
+       }
+
+       for (uint32_t i = 0; i < ZCG(num_warnings); i++) {
+               zend_recorded_warning *warning = ZCG(warnings)[i];
+               zend_string_release(warning->error_filename);
+               zend_string_release(warning->error_message);
+               efree(warning);
+       }
+       efree(ZCG(warnings));
+       ZCG(warnings) = NULL;
+       ZCG(num_warnings) = 0;
+}
+
 static zend_persistent_script *opcache_compile_file(zend_file_handle *file_handle, int type, const char *key, zend_op_array **op_array_p)
 {
        zend_persistent_script *new_persistent_script;
@@ -1739,6 +1790,9 @@ static zend_persistent_script *opcache_compile_file(zend_file_handle *file_handl
 
        /* Override them with ours */
        ZVAL_UNDEF(&EG(user_error_handler));
+       ZCG(record_warnings) = ZCG(accel_directives).record_warnings;
+       ZCG(num_warnings) = 0;
+       ZCG(warnings) = NULL;
 
        zend_try {
                orig_compiler_options = CG(compiler_options);
@@ -1761,9 +1815,11 @@ static zend_persistent_script *opcache_compile_file(zend_file_handle *file_handl
        /* Restore originals */
        CG(active_op_array) = orig_active_op_array;
        EG(user_error_handler) = orig_user_error_handler;
+       ZCG(record_warnings) = 0;
 
        if (!op_array) {
                /* compilation failed */
+               free_recorded_warnings();
                if (do_bailout) {
                        zend_bailout();
                }
@@ -1782,6 +1838,10 @@ static zend_persistent_script *opcache_compile_file(zend_file_handle *file_handl
                (op_array->fn_flags & ZEND_ACC_EARLY_BINDING) ?
                        zend_build_delayed_early_binding_list(op_array) :
                        (uint32_t)-1;
+       new_persistent_script->num_warnings = ZCG(num_warnings);
+       new_persistent_script->warnings = ZCG(warnings);
+       ZCG(num_warnings) = 0;
+       ZCG(warnings) = NULL;
 
        efree(op_array); /* we have valid persistent_script, so it's safe to free op_array */
 
@@ -1866,6 +1926,7 @@ zend_op_array *file_cache_compile_file(zend_file_handle *file_handle, int type)
                                }
                        }
                }
+               replay_warnings(persistent_script);
                zend_file_handle_dtor(file_handle);
 
            if (persistent_script->ping_auto_globals_mask) {
@@ -2187,6 +2248,7 @@ zend_op_array *persistent_compile_file(zend_file_handle *file_handle, int type)
                                }
                        }
                }
+               replay_warnings(persistent_script);
                zend_file_handle_dtor(file_handle);
                from_shared_memory = 1;
        }
@@ -3051,6 +3113,10 @@ file_cache_fallback:
        accelerator_orig_zend_resolve_path = zend_resolve_path;
        zend_resolve_path = persistent_zend_resolve_path;
 
+       /* Override error callback, so we can store errors that occur during compilation */
+       accelerator_orig_zend_error_cb = zend_error_cb;
+       zend_error_cb = persistent_error_cb;
+
        /* Override chdir() function */
        if ((func = zend_hash_str_find_ptr(CG(function_table), "chdir", sizeof("chdir")-1)) != NULL &&
            func->type == ZEND_INTERNAL_FUNCTION) {
index 7dd9267e8fe46b7e3db9828fbaff95987b6c24ab..05a05fa1b68a46c11e19412a9fc337ac22eb5c50 100644 (file)
@@ -109,6 +109,13 @@ typedef enum _zend_accel_restart_reason {
        ACCEL_RESTART_USER    /* restart scheduled by opcache_reset() */
 } zend_accel_restart_reason;
 
+typedef struct _zend_recorded_warning {
+       int type;
+       uint32_t error_lineno;
+       zend_string *error_filename;
+       zend_string *error_message;
+} zend_recorded_warning;
+
 typedef struct _zend_persistent_script {
        zend_script    script;
        zend_long      compiler_halt_offset;   /* position of __HALT_COMPILER or -1 */
@@ -117,6 +124,8 @@ typedef struct _zend_persistent_script {
        zend_bool      corrupted;
        zend_bool      is_phar;
        zend_bool      empty;
+       uint32_t       num_warnings;
+       zend_recorded_warning **warnings;
 
        void          *mem;                    /* shared memory area used by script structures */
        size_t         size;                   /* size of used shared memory */
@@ -151,6 +160,7 @@ typedef struct _zend_accel_directives {
        zend_bool      validate_timestamps;
        zend_bool      revalidate_path;
        zend_bool      save_comments;
+       zend_bool      record_warnings;
        zend_bool      protect_memory;
        zend_bool      file_override_enabled;
        zend_bool      enable_cli;
@@ -221,6 +231,10 @@ typedef struct _zend_accel_globals {
        void                   *arena_mem;
        zend_persistent_script *current_persistent_script;
        zend_bool               is_immutable_class;
+       /* Temporary storage for warnings before they are moved into persistent_script. */
+       zend_bool               record_warnings;
+       uint32_t                num_warnings;
+       zend_recorded_warning **warnings;
        /* cache to save hash lookup on the same INCLUDE opcode */
        const zend_op          *cache_opline;
        zend_persistent_script *cache_persistent_script;
diff --git a/ext/opcache/tests/warning_replay.inc b/ext/opcache/tests/warning_replay.inc
new file mode 100644 (file)
index 0000000..4931d79
--- /dev/null
@@ -0,0 +1,3 @@
+<?php
+
+declare(unknown=1);
diff --git a/ext/opcache/tests/warning_replay.phpt b/ext/opcache/tests/warning_replay.phpt
new file mode 100644 (file)
index 0000000..e8903bb
--- /dev/null
@@ -0,0 +1,15 @@
+--TEST--
+Opcache should replay compilation warnings if opcache.record_warnings=1
+--INI--
+opcache.record_warnings=1
+--FILE--
+<?php
+
+require __DIR__ . '/warning_replay.inc';
+require __DIR__ . '/warning_replay.inc';
+
+?>
+--EXPECTF--
+Warning: Unsupported declare 'unknown' in %swarning_replay.inc on line 3
+
+Warning: Unsupported declare 'unknown' in %swarning_replay.inc on line 3
index 151bc31123233c300663360c1318139bd9a3cd55..4a535591c0e37b262bc8949280072c3a0cdbc0d4 100644 (file)
@@ -280,6 +280,7 @@ ZEND_INI_BEGIN()
 
        STD_PHP_INI_BOOLEAN("opcache.protect_memory"        , "0"  , PHP_INI_SYSTEM, OnUpdateBool,                  accel_directives.protect_memory,            zend_accel_globals, accel_globals)
        STD_PHP_INI_BOOLEAN("opcache.save_comments"         , "1"  , PHP_INI_SYSTEM, OnUpdateBool,                  accel_directives.save_comments,             zend_accel_globals, accel_globals)
+       STD_PHP_INI_BOOLEAN("opcache.record_warnings"       , "0"  , PHP_INI_SYSTEM, OnUpdateBool,                  accel_directives.record_warnings,           zend_accel_globals, accel_globals)
 
        STD_PHP_INI_ENTRY("opcache.optimization_level"    , DEFAULT_OPTIMIZATION_LEVEL , PHP_INI_SYSTEM, OnUpdateLong, accel_directives.optimization_level,   zend_accel_globals, accel_globals)
        STD_PHP_INI_ENTRY("opcache.opt_debug_level"       , "0"      , PHP_INI_SYSTEM, OnUpdateLong,             accel_directives.opt_debug_level,            zend_accel_globals, accel_globals)
@@ -767,6 +768,7 @@ ZEND_FUNCTION(opcache_get_configuration)
 
        add_assoc_bool(&directives,   "opcache.protect_memory",         ZCG(accel_directives).protect_memory);
        add_assoc_bool(&directives,   "opcache.save_comments",          ZCG(accel_directives).save_comments);
+       add_assoc_bool(&directives,   "opcache.record_warnings",        ZCG(accel_directives).record_warnings);
        add_assoc_bool(&directives,   "opcache.enable_file_override",   ZCG(accel_directives).file_override_enabled);
        add_assoc_long(&directives,      "opcache.optimization_level",     ZCG(accel_directives).optimization_level);
 
index a853fe3c84ca0a9dd38e76a2e1d15717eedbde79..631d712ba4a383ef4c5512822d05b9c32fb2dd19 100644 (file)
@@ -802,6 +802,26 @@ static void zend_file_cache_serialize_class(zval                     *zv,
        ZEND_MAP_PTR_INIT(ce->static_members_table, &ce->default_static_members_table);
 }
 
+static void zend_file_cache_serialize_warnings(
+               zend_persistent_script *script, zend_file_cache_metainfo *info, void *buf)
+{
+       if (script->warnings) {
+               zend_recorded_warning **warnings;
+               SERIALIZE_PTR(script->warnings);
+               warnings = script->warnings;
+               UNSERIALIZE_PTR(warnings);
+
+               for (uint32_t i = 0; i < script->num_warnings; i++) {
+                       zend_recorded_warning *warning;
+                       SERIALIZE_PTR(warnings[i]);
+                       warning = warnings[i];
+                       UNSERIALIZE_PTR(warning);
+                       SERIALIZE_STR(warning->error_filename);
+                       SERIALIZE_STR(warning->error_message);
+               }
+       }
+}
+
 static void zend_file_cache_serialize(zend_persistent_script   *script,
                                       zend_file_cache_metainfo *info,
                                       void                     *buf)
@@ -823,6 +843,7 @@ static void zend_file_cache_serialize(zend_persistent_script   *script,
        zend_file_cache_serialize_hash(&new_script->script.class_table, script, info, buf, zend_file_cache_serialize_class);
        zend_file_cache_serialize_hash(&new_script->script.function_table, script, info, buf, zend_file_cache_serialize_func);
        zend_file_cache_serialize_op_array(&new_script->script.main_op_array, script, info, buf);
+       zend_file_cache_serialize_warnings(new_script, info, buf);
 
        SERIALIZE_PTR(new_script->arena_mem);
        new_script->mem = NULL;
@@ -1506,6 +1527,18 @@ static void zend_file_cache_unserialize_class(zval                    *zv,
        }
 }
 
+static void zend_file_cache_unserialize_warnings(zend_persistent_script *script, void *buf)
+{
+       if (script->warnings) {
+               UNSERIALIZE_PTR(script->warnings);
+               for (uint32_t i = 0; i < script->num_warnings; i++) {
+                       UNSERIALIZE_PTR(script->warnings[i]);
+                       UNSERIALIZE_STR(script->warnings[i]->error_filename);
+                       UNSERIALIZE_STR(script->warnings[i]->error_message);
+               }
+       }
+}
+
 static void zend_file_cache_unserialize(zend_persistent_script  *script,
                                         void                    *buf)
 {
@@ -1518,6 +1551,7 @@ static void zend_file_cache_unserialize(zend_persistent_script  *script,
        zend_file_cache_unserialize_hash(&script->script.function_table,
                        script, buf, zend_file_cache_unserialize_func, ZEND_FUNCTION_DTOR);
        zend_file_cache_unserialize_op_array(&script->script.main_op_array, script, buf);
+       zend_file_cache_unserialize_warnings(script, buf);
 
        UNSERIALIZE_PTR(script->arena_mem);
 }
index 23f1a5fb61e5fd5bf2138242b3e097e58bbccacb..ccdc3cbdb9acc9ea4134c6d57c68fdf28a06f19c 100644 (file)
@@ -1087,6 +1087,19 @@ static void zend_accel_persist_class_table(HashTable *class_table)
        } ZEND_HASH_FOREACH_END();
 }
 
+static void zend_persist_warnings(zend_persistent_script *script) {
+       if (script->warnings) {
+               script->warnings = zend_shared_memdup_free(
+                       script->warnings, script->num_warnings * sizeof(zend_recorded_warning *));
+               for (uint32_t i = 0; i < script->num_warnings; i++) {
+                       script->warnings[i] = zend_shared_memdup_free(
+                               script->warnings[i], sizeof(zend_recorded_warning));
+                       zend_accel_store_string(script->warnings[i]->error_filename);
+                       zend_accel_store_string(script->warnings[i]->error_message);
+               }
+       }
+}
+
 zend_persistent_script *zend_accel_script_persist(zend_persistent_script *script, const char **key, unsigned int key_length, int for_shm)
 {
        Bucket *p;
@@ -1136,6 +1149,7 @@ zend_persistent_script *zend_accel_script_persist(zend_persistent_script *script
                zend_persist_op_array(&p->val);
        } ZEND_HASH_FOREACH_END();
        zend_persist_op_array_ex(&script->script.main_op_array, script);
+       zend_persist_warnings(script);
 
        if (for_shm) {
                ZCSG(map_ptr_last) = CG(map_ptr_last);
index 5de27b9efb0064407112c7b13cbed7a38d6a5af1..8a1e99c65df279b8df7e67ec49561e3c888e64c7 100644 (file)
@@ -514,6 +514,15 @@ static void zend_accel_persist_class_table_calc(HashTable *class_table)
        } ZEND_HASH_FOREACH_END();
 }
 
+static void zend_persist_warnings_calc(zend_persistent_script *script) {
+       ADD_SIZE(script->num_warnings * sizeof(zend_recorded_warning *));
+       for (uint32_t i = 0; i < script->num_warnings; i++) {
+               ADD_SIZE(sizeof(zend_recorded_warning));
+               ADD_STRING(script->warnings[i]->error_filename);
+               ADD_STRING(script->warnings[i]->error_message);
+       }
+}
+
 uint32_t zend_accel_script_persist_calc(zend_persistent_script *new_persistent_script, const char *key, unsigned int key_length, int for_shm)
 {
        Bucket *p;
@@ -556,6 +565,7 @@ uint32_t zend_accel_script_persist_calc(zend_persistent_script *new_persistent_s
                zend_persist_op_array_calc(&p->val);
        } ZEND_HASH_FOREACH_END();
        zend_persist_op_array_calc_ex(&new_persistent_script->script.main_op_array);
+       zend_persist_warnings_calc(new_persistent_script);
 
 #if defined(__AVX__) || defined(__SSE2__)
        /* Align size to 64-byte boundary */
index 5ef32525b10db52cca89a01091d1fa7884e73e8b..1fcd0e45a2a9b68e5897268494f2d785b49e4e01 100644 (file)
@@ -1776,6 +1776,11 @@ ldap.max_links = -1
 ; size of the optimized code.
 ;opcache.save_comments=1
 
+; If enabled, compilation warnings (including notices and deprecations) will
+; be recorded and replayed each time a file is included. Otherwise, compilation
+; warnings will only be emitted when the file is first cached.
+;opcache.record_warnings=0
+
 ; Allow file existence override (file_exists, etc.) performance feature.
 ;opcache.enable_file_override=0
 
index fd9cffa6393b6d541429a1d60bfdc19c8ea1f329..761b0c21072833708d0f2b462a578b38bfa9b786 100644 (file)
@@ -1778,6 +1778,11 @@ ldap.max_links = -1
 ; size of the optimized code.
 ;opcache.save_comments=1
 
+; If enabled, compilation warnings (including notices and deprecations) will
+; be recorded and replayed each time a file is included. Otherwise, compilation
+; warnings will only be emitted when the file is first cached.
+;opcache.record_warnings=0
+
 ; Allow file existence override (file_exists, etc.) performance feature.
 ;opcache.enable_file_override=0