diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..8349fd6 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,75 @@ +name: Build + +on: + push: + branches: + - main + - phase2-observer-migration + pull_request: + workflow_dispatch: + inputs: + php-version: + description: 'PHP version to build' + required: false + default: '8.5' + type: choice + options: + - '8.2' + - '8.3' + - '8.4' + - '8.5' + +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + - os: macos-latest + name: macOS (Apple Silicon) + - os: ubuntu-latest + name: Linux (x86_64) + runs-on: ${{ matrix.os }} + name: "${{ matrix.name }} — PHP ${{ inputs.php-version || '8.5' }}" + steps: + - uses: actions/checkout@v6 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ inputs.php-version || '8.5' }} + coverage: none + extensions: xmlreader + + - name: Build extension + run: | + phpize + ./configure --enable-xdebug + make -j$(sysctl -n hw.logicalcpu 2>/dev/null || nproc) + + - name: Verify extension loads + run: | + php -dzend_extension=$PWD/modules/xdebug.so -v + php -dzend_extension=$PWD/modules/xdebug.so -r "echo 'xdebug_info() works: ' . (function_exists('xdebug_info') ? 'yes' : 'no') . PHP_EOL;" + + - name: Smoke test (DBGp) + run: | + TEST_PHP_ARGS="-n -dzend_extension=$PWD/modules/xdebug.so" + echo "TEST_PHP_ARGS=$TEST_PHP_ARGS" >> $GITHUB_ENV + export TEST_PHP_ARGS + php -n run-xdebug-tests.php -q tests/debugger/bug00530.phpt + + - name: Get build info + id: info + run: | + PHP_VER=$(php -r "echo PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION;") + OS_TAG=$(echo "${{ matrix.os }}" | sed 's/-latest//') + ARCH=$(uname -m) + echo "artifact_name=xdebug-php${PHP_VER}-${OS_TAG}-${ARCH}" >> $GITHUB_OUTPUT + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.info.outputs.artifact_name }} + path: modules/xdebug.so + retention-days: 90 diff --git a/.gitignore b/.gitignore index b3fc50d..6f63e54 100644 --- a/.gitignore +++ b/.gitignore @@ -98,3 +98,6 @@ src/*/*.dep src/*/*.lo src/*/*/*.dep src/*/*/*.lo +*.bak +*.phase1 +.libs/ diff --git a/src/base/base.c b/src/base/base.c index ea2d283..311217d 100644 --- a/src/base/base.c +++ b/src/base/base.c @@ -66,8 +66,6 @@ static void xdebug_error_cb(int orig_type, const char *error_filename, const uin /* execution redirection functions */ zend_op_array* (*old_compile_file)(zend_file_handle* file_handle, int type); -static void (*xdebug_old_execute_ex)(zend_execute_data *execute_data); -static void (*xdebug_old_execute_internal)(zend_execute_data *execute_data, zval *return_value); /* error_cb and execption hook overrides */ void xdebug_base_use_original_error_cb(void); @@ -93,7 +91,10 @@ static zend_op_array *xdebug_compile_file(zend_file_handle *file_handle, int typ return NULL; } - xdebug_debugger_compile_file(op_array); + /* Only process compiled files when debugger is active */ + if (XG_BASE(observer_active)) { + xdebug_debugger_compile_file(op_array); + } return op_array; } @@ -712,6 +713,12 @@ static void xdebug_execute_user_code_begin(zend_execute_data *execute_data) xdebug_debug_init_if_requested_at_startup(); } + /* After first-call init, deactivate observer if no debugger connected */ + XG_BASE(needs_debug_init) = 0; + if (!xdebug_is_debug_connection_active()) { + XG_BASE(observer_active) = 0; + return; + } } fse = xdebug_add_stack_frame(execute_data, op_array, XDEBUG_USER_DEFINED); @@ -789,38 +796,7 @@ static bool should_run_user_handler(zend_execute_data *execute_data) return true; } -/* This is confusing. On PHP 8.1 we flip the logic, as normal user functions - * are handled through the Observer API. Once PHP 8.0 support is dropped, the - * negation should be **added** to the usage below in xdebug_execute_ex. */ -static bool should_run_user_handler_wrapper(zend_execute_data *execute_data) -{ - /* If the stack vector hasn't been initialised yet, we should abort immediately */ - if (!XG_BASE(stack)) { - return false; - } - -#if PHP_VERSION_ID >= 80100 - return !should_run_user_handler(execute_data); -#else - return should_run_user_handler(execute_data); -#endif -} - -/* We still need this to do "include", "require", and "eval" */ -static void xdebug_execute_ex(zend_execute_data *execute_data) -{ - bool run_user_handler = should_run_user_handler_wrapper(execute_data); - - if (run_user_handler) { - xdebug_execute_user_code_begin(execute_data); - } - xdebug_old_execute_ex(execute_data); - - if (run_user_handler) { - xdebug_execute_user_code_end(execute_data, execute_data->return_value); - } -} static int check_soap_call(function_stack_entry *fse, zend_execute_data *execute_data) { @@ -893,7 +869,7 @@ static void xdebug_execute_internal_end(zend_execute_data *execute_data, zval *r function_stack_entry *fse; /* Re-acquire the tail as nested calls through - * xdebug_old_execute_internal() might have reallocated the vector */ + * nested calls might have reallocated the vector */ fse = XDEBUG_VECTOR_TAIL(XG_BASE(stack)); /* Restore SOAP situation if needed */ @@ -911,30 +887,14 @@ static void xdebug_execute_internal_end(zend_execute_data *execute_data, zval *r } } -#if PHP_VERSION_ID < 80200 -static void xdebug_execute_internal(zend_execute_data *execute_data, zval *return_value) -{ - bool run_internal_handler = should_run_internal_handler(execute_data); - - if (run_internal_handler) { - xdebug_execute_internal_begin(execute_data); - } - - if (xdebug_old_execute_internal) { - xdebug_old_execute_internal(execute_data, return_value); - } else { - execute_internal(execute_data, return_value); - } - - if (run_internal_handler) { - xdebug_execute_internal_end(execute_data, return_value); - } -} -#endif -#if PHP_VERSION_ID >= 80100 static void xdebug_execute_begin(zend_execute_data *execute_data) { + /* Fast path: skip all work when no debug session is active */ + if (EXPECTED(!XG_BASE(observer_active))) { + return; + } + /* If the stack vector hasn't been initialised yet, we should abort immediately */ if (!XG_BASE(stack)) { return; @@ -943,15 +903,18 @@ static void xdebug_execute_begin(zend_execute_data *execute_data) if (should_run_user_handler(execute_data)) { xdebug_execute_user_code_begin(execute_data); } -#if PHP_VERSION_ID >= 80200 if (should_run_internal_handler(execute_data)) { xdebug_execute_internal_begin(execute_data); } -#endif } static void xdebug_execute_end(zend_execute_data *execute_data, zval *retval) { + /* Fast path: skip all work when no debug session is active */ + if (EXPECTED(!XG_BASE(observer_active))) { + return; + } + /* If the stack vector hasn't been initialised yet, we should abort immediately */ if (!XG_BASE(stack)) { return; @@ -960,18 +923,19 @@ static void xdebug_execute_end(zend_execute_data *execute_data, zval *retval) if (should_run_user_handler(execute_data)) { xdebug_execute_user_code_end(execute_data, retval); } -#if PHP_VERSION_ID >= 80200 if (should_run_internal_handler(execute_data)) { xdebug_execute_internal_end(execute_data, retval); } -#endif } static zend_observer_fcall_handlers xdebug_observer_init(zend_execute_data *execute_data) { + /* If debug mode is not enabled or observer is deactivated (no debugger connected), skip */ + if (!XDEBUG_MODE_IS(XDEBUG_MODE_STEP_DEBUG) || !XG_BASE(observer_active)) { + return (zend_observer_fcall_handlers){NULL, NULL}; + } return (zend_observer_fcall_handlers){xdebug_execute_begin, xdebug_execute_end}; } -#endif /***************************************************************************/ static void xdebug_base_overloaded_functions_setup(void) @@ -1024,7 +988,6 @@ static int xdebug_closure_serialize_deny_wrapper(zval *object, unsigned char **b return FAILURE; } -#if PHP_VERSION_ID >= 80100 /** Handling fibers ********************************************************/ static struct xdebug_fiber_entry* xdebug_fiber_entry_ctor(xdebug_vector *stack) { @@ -1120,7 +1083,6 @@ static void xdebug_fiber_switch_observer(zend_fiber_context *from, zend_fiber_co zend_string_release(to_key); } /***************************************************************************/ -#endif #ifdef __linux__ int read_systemd_private_tmp_directory(char **private_tmp) @@ -1197,28 +1159,14 @@ void xdebug_base_minit(INIT_FUNC_ARGS) xdebug_old_error_cb = zend_error_cb; xdebug_new_error_cb = xdebug_error_cb; -#if PHP_VERSION_ID >= 80100 - /* User Code Functions */ + /* User Code + Internal Functions via Observer API */ zend_observer_fcall_register(xdebug_observer_init); -#endif - - /* Include, Require, Eval */ - xdebug_old_execute_ex = zend_execute_ex; - zend_execute_ex = xdebug_execute_ex; - -#if PHP_VERSION_ID < 80200 - /* Internal Functions, since 8.2 they're also observed */ - xdebug_old_execute_internal = zend_execute_internal; - zend_execute_internal = xdebug_execute_internal; -#endif XG_BASE(error_reporting_override) = 0; XG_BASE(error_reporting_overridden) = 0; XG_BASE(output_is_tty) = OUTPUT_NOT_CHECKED; -#if PHP_VERSION_ID >= 80100 zend_observer_fiber_switch_register(xdebug_fiber_switch_observer); -#endif XG_BASE(private_tmp) = NULL; #ifdef __linux__ @@ -1240,10 +1188,8 @@ void xdebug_base_minit(INIT_FUNC_ARGS) void xdebug_base_mshutdown() { - /* Reset compile, execute and error callbacks */ + /* Reset compile and error callbacks */ zend_compile_file = old_compile_file; - zend_execute_ex = xdebug_old_execute_ex; - zend_execute_internal = xdebug_old_execute_internal; zend_error_cb = xdebug_old_error_cb; #ifdef __linux__ @@ -1272,7 +1218,6 @@ void xdebug_base_rinit() xdebug_base_use_xdebug_throw_exception_hook(); } -#if PHP_VERSION_ID >= 80100 { zend_string *fiber_key = create_key_for_fiber(EG(main_fiber_context)); @@ -1281,9 +1226,6 @@ void xdebug_base_rinit() zend_string_release(fiber_key); } -#else - XG_BASE(stack) = xdebug_vector_alloc(sizeof(function_stack_entry), function_stack_entry_dtor); -#endif XG_BASE(in_debug_info) = 0; XG_BASE(prev_memory) = 0; XG_BASE(function_count) = -1; @@ -1329,6 +1271,10 @@ void xdebug_base_rinit() /* Signal that we're in a request now */ XG_BASE(in_execution) = 1; + /* Observer starts active to allow first-call debug init check */ + XG_BASE(observer_active) = XDEBUG_MODE_IS(XDEBUG_MODE_STEP_DEBUG); + XG_BASE(needs_debug_init) = XDEBUG_MODE_IS(XDEBUG_MODE_STEP_DEBUG); + /* filters */ XG_BASE(filter_type_stack) = XDEBUG_FILTER_NONE; XG_BASE(filters_stack) = xdebug_llist_alloc(xdebug_llist_string_dtor); @@ -1341,12 +1287,8 @@ void xdebug_base_rinit() void xdebug_base_post_deactivate() { -#if PHP_VERSION_ID >= 80100 xdebug_hash_destroy(XG_BASE(fiber_stacks)); XG_BASE(fiber_stacks) = NULL; -#else - xdebug_vector_destroy(XG_BASE(stack)); -#endif XG_BASE(stack) = NULL; XG_BASE(in_debug_info) = 0; diff --git a/src/base/base_globals.h b/src/base/base_globals.h index fe0af12..fbf909b 100644 --- a/src/base/base_globals.h +++ b/src/base/base_globals.h @@ -59,6 +59,8 @@ typedef struct _xdebug_base_globals_t { /* in-execution checking */ zend_bool in_execution; + zend_bool observer_active; /* true when debug session is active or needs init */ + zend_bool needs_debug_init; /* true until first-call debug init check is done */ zend_bool in_var_serialisation; /* Systemd Private Temp */ diff --git a/src/debugger/com.c b/src/debugger/com.c index 81e4250..27ba491 100644 --- a/src/debugger/com.c +++ b/src/debugger/com.c @@ -666,6 +666,8 @@ void xdebug_mark_debug_connection_active() { XG_DBG(remote_connection_enabled) = 1; XG_DBG(remote_connection_pid) = xdebug_get_pid(); + /* Activate observer when debugger connects (including mid-request via xdebug_connect_to_client) */ + XG_BASE(observer_active) = 1; } void xdebug_mark_debug_connection_pending() diff --git a/src/debugger/handler_dbgp.c b/src/debugger/handler_dbgp.c index b46bd6c..2dad3bf 100644 --- a/src/debugger/handler_dbgp.c +++ b/src/debugger/handler_dbgp.c @@ -3118,13 +3118,7 @@ int xdebug_dbgp_notification(xdebug_con *context, xdebug_str *filename, long lin xdebug_xml_add_attribute_ex(error_container, "type", xdstrdup(type_string), 0, 1); } if (message) { - char *tmp_buf; - - if (type == E_ERROR && ((tmp_buf = xdebug_strip_php_stack_trace(message)) != NULL)) { - xdebug_xml_add_text(error_container, tmp_buf); - } else { - xdebug_xml_add_text(error_container, xdstrdup(message)); - } + xdebug_xml_add_text(error_container, xdstrdup(message)); } xdebug_xml_add_child(response, error_container); diff --git a/src/lib/lib.c b/src/lib/lib.c index 7654a17..5191453 100644 --- a/src/lib/lib.c +++ b/src/lib/lib.c @@ -53,11 +53,7 @@ void xdebug_library_zend_shutdown(void) void xdebug_library_minit(void) { - xdebug_set_opcode_multi_handler(ZEND_ASSIGN); - xdebug_set_opcode_multi_handler(ZEND_ASSIGN_DIM); - xdebug_set_opcode_multi_handler(ZEND_ASSIGN_OBJ); - xdebug_set_opcode_multi_handler(ZEND_ASSIGN_STATIC_PROP); - xdebug_set_opcode_multi_handler(ZEND_QM_ASSIGN); + /* ZEND_ASSIGN* handlers removed: were only needed for tracing/coverage (stripped in Phase 1) */ xdebug_set_opcode_multi_handler(ZEND_INCLUDE_OR_EVAL); } diff --git a/xdebug.c b/xdebug.c index 929d7de..0fd88fa 100644 --- a/xdebug.c +++ b/xdebug.c @@ -472,11 +472,42 @@ PHP_RINIT_FUNCTION(xdebug) xdebug_init_auto_globals(); - /* Only enabled extended info when it is not disabled */ - CG(compiler_options) = CG(compiler_options) | ZEND_COMPILE_EXTENDED_STMT; - + /* Early debug init: attempt connection at RINIT so observer_active is set + * before any user code runs. This allows xdebug_observer_init to return + * {NULL, NULL} for functions first-called when no debugger is connected. */ xdebug_base_rinit(); + if (XDEBUG_MODE_IS(XDEBUG_MODE_STEP_DEBUG)) { + /* Check early if debugging could be requested this request. + * For start_with_request=default (trigger mode), check if any + * trigger is present. If not, disable all heavy hooks for + * near-zero overhead. The actual connection happens on first + * function call if triggers are present. */ + /* Check if debugging could be requested this request. + * For trigger/default mode: check triggers, cookies, env vars. + * For yes mode: always expect a connection. + * For no mode: no debugging will happen. + * Note: xdebug_break() can initiate connections without triggers, + * but it handles re-enabling the observer itself. */ + int debug_requested = + xdebug_lib_start_with_request(XDEBUG_MODE_STEP_DEBUG) || + xdebug_lib_start_with_trigger(XDEBUG_MODE_STEP_DEBUG, NULL) || + xdebug_lib_start_upon_error() || + getenv("XDEBUG_SESSION_START") != NULL; + + if (debug_requested) { + /* Debug session likely: enable extended stmt opcodes */ + CG(compiler_options) = CG(compiler_options) | ZEND_COMPILE_EXTENDED_STMT; + } else { + /* No debug trigger: disable all heavy hooks for near-zero overhead. + * Note: xdebug_break() jit mode won't have full stepping support + * without EXT_STMT opcodes. Use start_with_request=yes or a trigger + * for full debugging support. */ + XG_BASE(observer_active) = 0; + XG_BASE(statement_handler_enabled) = false; + } + } + return SUCCESS; }