]> granicus.if.org Git - php/commitdiff
Stabilize watchpoints
authorBob Weinand <bobwei9@hotmail.com>
Sun, 19 Apr 2015 10:53:06 +0000 (12:53 +0200)
committerBob Weinand <bobwei9@hotmail.com>
Sun, 19 Apr 2015 10:53:29 +0000 (12:53 +0200)
sapi/phpdbg/phpdbg.h
sapi/phpdbg/phpdbg_utils.c
sapi/phpdbg/phpdbg_utils.h
sapi/phpdbg/phpdbg_watch.c
sapi/phpdbg/phpdbg_watch.h

index 5fe35674e9beeaf2c3637b54b7c60cb3ecc61a74..c636a7c791cf99e0b9a74b8e29a309de7ced82df 100644 (file)
@@ -248,6 +248,7 @@ ZEND_BEGIN_MODULE_GLOBALS(phpdbg)
        zend_llist watchlist_mem;                    /* triggered watchpoints */
        zend_bool watchpoint_hit;                    /* a watchpoint was hit */
        void (*original_free_function)(void *);      /* the original AG(mm_heap)->_free function */
+       phpdbg_watchpoint_t *watch_tmp;              /* temporary pointer for a watchpoint */
 
        char *exec;                                  /* file to execute */
        size_t exec_len;                             /* size of exec */
index 7a7b70ceaf70bc1b330f979f4a1bd3149f1b020b..a4725bd7440bd46a0bf868e52ec135128109057d 100644 (file)
@@ -399,10 +399,10 @@ static int phpdbg_parse_variable_arg_wrapper(char *name, size_t len, char *keyna
 }
 
 PHPDBG_API int phpdbg_parse_variable(char *input, size_t len, HashTable *parent, size_t i, phpdbg_parse_var_func callback, zend_bool silent) {
-       return phpdbg_parse_variable_with_arg(input, len, parent, i, (phpdbg_parse_var_with_arg_func) phpdbg_parse_variable_arg_wrapper, silent, callback);
+       return phpdbg_parse_variable_with_arg(input, len, parent, i, (phpdbg_parse_var_with_arg_func) phpdbg_parse_variable_arg_wrapper, NULL, silent, callback);
 }
 
-PHPDBG_API int phpdbg_parse_variable_with_arg(char *input, size_t len, HashTable *parent, size_t i, phpdbg_parse_var_with_arg_func callback, zend_bool silent, void *arg) {
+PHPDBG_API int phpdbg_parse_variable_with_arg(char *input, size_t len, HashTable *parent, size_t i, phpdbg_parse_var_with_arg_func callback, phpdbg_parse_var_with_arg_func step_cb, zend_bool silent, void *arg) {
        int ret = FAILURE;
        zend_bool new_index = 1;
        char *last_index;
@@ -469,10 +469,34 @@ PHPDBG_API int phpdbg_parse_variable_with_arg(char *input, size_t len, HashTable
                                        }
 
                                        ret = callback(name, namelen, keyname, index_len, parent, zv, arg) == SUCCESS || ret == SUCCESS?SUCCESS:FAILURE;
-                               } else if (Z_TYPE_P(zv) == IS_OBJECT) {
-                                       phpdbg_parse_variable_with_arg(input, len, Z_OBJPROP_P(zv), i, callback, silent, arg);
+                               } else retry_ref: if (Z_TYPE_P(zv) == IS_OBJECT) {
+                                       if (step_cb) {
+                                               char *name = estrndup(input, i);
+                                               char *keyname = estrndup(last_index, index_len);
+
+                                               ret = step_cb(name, i, keyname, index_len, parent, zv, arg) == SUCCESS || ret == SUCCESS?SUCCESS:FAILURE;
+                                       }
+
+                                       phpdbg_parse_variable_with_arg(input, len, Z_OBJPROP_P(zv), i, callback, step_cb, silent, arg);
                                } else if (Z_TYPE_P(zv) == IS_ARRAY) {
-                                       phpdbg_parse_variable_with_arg(input, len, Z_ARRVAL_P(zv), i, callback, silent, arg);
+                                       if (step_cb) {
+                                               char *name = estrndup(input, i);
+                                               char *keyname = estrndup(last_index, index_len);
+
+                                               ret = step_cb(name, i, keyname, index_len, parent, zv, arg) == SUCCESS || ret == SUCCESS?SUCCESS:FAILURE;
+                                       }
+
+                                       phpdbg_parse_variable_with_arg(input, len, Z_ARRVAL_P(zv), i, callback, step_cb, silent, arg);
+                               } else if (Z_ISREF_P(zv)) {
+                                       if (step_cb) {
+                                               char *name = estrndup(input, i);
+                                               char *keyname = estrndup(last_index, index_len);
+
+                                               ret = step_cb(name, i, keyname, index_len, parent, zv, arg) == SUCCESS || ret == SUCCESS?SUCCESS:FAILURE;
+                                       }
+
+                                       ZVAL_DEREF(zv);
+                                       goto retry_ref;
                                } else {
                                        /* Ignore silently */
                                }
@@ -493,14 +517,38 @@ PHPDBG_API int phpdbg_parse_variable_with_arg(char *input, size_t len, HashTable
 
                        last_index[index_len] = last_chr;
                        if (i == len) {
-                               char *name = estrndup(input, len);
+                               char *name = estrndup(input, i);
                                char *keyname = estrndup(last_index, index_len);
 
-                               ret = callback(name, len, keyname, index_len, parent, zv, arg) == SUCCESS || ret == SUCCESS?SUCCESS:FAILURE;
-                       } else if (Z_TYPE_P(zv) == IS_OBJECT) {
+                               ret = callback(name, i, keyname, index_len, parent, zv, arg) == SUCCESS || ret == SUCCESS?SUCCESS:FAILURE;
+                       } else retry_ref_end: if (Z_TYPE_P(zv) == IS_OBJECT) {
+                               if (step_cb) {
+                                       char *name = estrndup(input, i);
+                                       char *keyname = estrndup(last_index, index_len);
+
+                                       ret = step_cb(name, i, keyname, index_len, parent, zv, arg) == SUCCESS || ret == SUCCESS?SUCCESS:FAILURE;
+                               }
+
                                parent = Z_OBJPROP_P(zv);
                        } else if (Z_TYPE_P(zv) == IS_ARRAY) {
+                               if (step_cb) {
+                                       char *name = estrndup(input, i);
+                                       char *keyname = estrndup(last_index, index_len);
+
+                                       ret = step_cb(name, i, keyname, index_len, parent, zv, arg) == SUCCESS || ret == SUCCESS?SUCCESS:FAILURE;
+                               }
+
                                parent = Z_ARRVAL_P(zv);
+                       } else if (Z_ISREF_P(zv)) {
+                               if (step_cb) {
+                                       char *name = estrndup(input, i);
+                                       char *keyname = estrndup(last_index, index_len);
+
+                                       ret = step_cb(name, i, keyname, index_len, parent, zv, arg) == SUCCESS || ret == SUCCESS?SUCCESS:FAILURE;
+                               }
+
+                               ZVAL_DEREF(zv);
+                               goto retry_ref_end;
                        } else {
                                phpdbg_error("variable", "type=\"notiterable\" variable=\"%.*s\"", "%.*s is nor an array nor an object", (int) (input[i] == '>' ? i - 1 : i), input);
                                return FAILURE;
index 911be725cf723cf1f658b26e632f11f4be92afa3..b835ace7034e5bcf9b549601a5c3e59c3dff473c 100644 (file)
@@ -88,7 +88,7 @@ typedef int (*phpdbg_parse_var_func)(char *name, size_t len, char *keyname, size
 typedef int (*phpdbg_parse_var_with_arg_func)(char *name, size_t len, char *keyname, size_t keylen, HashTable *parent, zval *zv, void *arg);
 
 PHPDBG_API int phpdbg_parse_variable(char *input, size_t len, HashTable *parent, size_t i, phpdbg_parse_var_func callback, zend_bool silent);
-PHPDBG_API int phpdbg_parse_variable_with_arg(char *input, size_t len, HashTable *parent, size_t i, phpdbg_parse_var_with_arg_func callback, zend_bool silent, void *arg);
+PHPDBG_API int phpdbg_parse_variable_with_arg(char *input, size_t len, HashTable *parent, size_t i, phpdbg_parse_var_with_arg_func callback, phpdbg_parse_var_with_arg_func step_cb, zend_bool silent, void *arg);
 
 int phpdbg_is_auto_global(char *name, int len);
 
index bb717682823a6ca7a659fdc331743e0944ba3ed6..49f8885b90423c058f8a4b86ed0638221ac133de 100644 (file)
    +----------------------------------------------------------------------+
 */
 
+/* Some information for the reader...
+ *
+ * Watchpoints are either simple, recursive or implicit (PHPDBG_WATCH_* flags)
+ * Simple means that a particular watchpoint was explicitely defined
+ * Recursive watchpoints are created recursively and substitute simple watchpoints
+ * Implicit watchpoints are implicitely created on all ancestors of simple or recursive watchpoints
+ * Recursive and (simple or implicit) watchpoints are mutually exclusive
+ *
+ * PHPDBG_G(watchpoint_tree) contains all watchpoints identified by the watch target address
+ * PHPDBG_G(watch_HashTables) contains the dtors of the HashTables to call in our custom dtor (we substitute the dtor of HashTables containing watched zvals by our own dtor)
+ * PHPDBG_G(watchpoints) contains all watchpoints (except the ones managed by watch collision)
+ * PHPDBG_G(watch_collisions) is indexed by a zend_reference * pointer. It stores information about collisions (everything which contains a zend_reference * may be referenced by multiple watches)
+ *
+ * Creating a watchpoint:
+ * * Create watchpoints with PHPDBG_WATCH_IMPLICIT set on each zval and HashTable in hierarchy except the last zval or HashTable fetch. (if already existing PHPDBG_WATCH_IMPLICIT flag is added)
+ * * Create a PHPDBG_WATCH_SIMPLE watch for simple watches or a PHPDBG_WATCH_RECURSIVE watch for recursive watches
+ * * When the target zval is an IS_REFERENCE, create a watchpoint on it too
+ * * Each time a watchpoints parent is a zval and it is Z_REFCOUNTED(), put a watchpoint (WATCH_ON_REFCOUNTED) on it and add a watchpoint collision
+ * * When in recursive mode and encountering a not-refcounted PHPDBG_WATCH_SIMPLE, remove it and recreate it with a PHPDBG_WATCH_RECURSIVE (handled via watch collision)
+ * * Make attention to not add something twice or iterate over it twice
+ *
+ * Deleting a watchpoint:
+ * * Only allow deletion of recursive watches at their root and simple watches
+ * * Go to referenced variable. And remove watch collision on *parent* (if there is a parent)
+ * * If it is Z_REFCOUNTED(), remove that watch collision
+ *
+ * Watch collisions:
+ * * hold a counter for recursive, if it is incremented from 0 to 1, create recursive watchpoint
+ * * holds a HashTable for normal (not implicit) watchpoints ... it is used to get the fetch type of the HashTable (depending on whether it is empty or not)
+ * * holds a HashTable for implicit watchpoints ... (some sort of a refcounter, but ensure that there are no duplicates)
+ * * if normal and implicit watchpoints are empty, drop that watch collision and remove WATCH_ON_REFCOUNT alongside with watchpoint on an eventual reference
+ *
+ * Watching on addresses:
+ * * Address and size are transformed into memory page aligned address and size
+ * * mprotect() enables or disables them (depending on flags) - Windows has a transparent compatibility layer in phpdbg_win.c
+ * * segfault handler dumps watched memory segment and deactivates watchpoint
+ * * later watches inside these memory segments are compared against their current value and eventually reactivated (or deleted)
+ *
+ * A watched zval was removed:
+ * * trigger a memory copy (in segv handler) and an automatic deactivation
+ * * change a type flag _zval_struct.u1.v.type_flags (add PHPDBG_DESTRUCTED_ZVAL flag) in memory dump
+ *
+ * A watched zval was changed:
+ * * check if parent container has a different reference for referenced zval - recursively update all watches and drop them if necessary
+ * * if _zval_struct.u1.v.type_flags & PHPDBG_DESTRUCTED_ZVAL, add it to a list of zvals to be handled at the end (if location was not changed, remove it finally)
+ * * display changes if watch->flags & PHPDBG_WATCH_NORMAL (means: not implicit)
+ * * handle case where Z_RECOUNTED() or Z_PTR() changed (remove/add collison(s))
+ * * if necessary ... on zvals: handle references, if recursive also objects and arrays ... on arrays: if recursive, add new elements
+ * * drop destructed zval watchpoints which were not updated
+ */
+
 #include "zend.h"
 #include "phpdbg.h"
 #include "phpdbg_btree.h"
@@ -41,6 +92,10 @@ const phpdbg_command_t phpdbg_watch_commands[] = {
 //#define HT_FROM_WATCH(watch) (watch->type == WATCH_ON_OBJECT ? watch->addr.obj->handlers->get_properties(watch->parent_container.zv) : watch->type == WATCH_ON_ARRAY ? &watch->addr.arr->ht : NULL)
 #define HT_FROM_ZVP(zvp) (Z_TYPE_P(zvp) == IS_OBJECT ? Z_OBJPROP_P(zvp) : Z_TYPE_P(zvp) == IS_ARRAY ? Z_ARRVAL_P(zvp) : NULL)
 
+#define HT_WATCH_OFFSET (sizeof(zend_refcounted *) + sizeof(uint32_t)) /* we are not interested in gc and flags */
+#define HT_PTR_HT(ptr) ((HashTable *) (((char *) (ptr)) - HT_WATCH_OFFSET))
+#define HT_WATCH_HT(watch) HT_PTR_HT((watch)->addr.ptr)
+
 typedef struct {
        void *page;
        size_t size;
@@ -103,73 +158,132 @@ void phpdbg_create_zval_watchpoint(zval *zv, phpdbg_watchpoint_t *watch) {
 }
 
 void phpdbg_create_ht_watchpoint(HashTable *ht, phpdbg_watchpoint_t *watch) {
-       phpdbg_create_addr_watchpoint(ht, sizeof(HashTable), watch);
+       phpdbg_create_addr_watchpoint(((char *) ht) + HT_WATCH_OFFSET, sizeof(HashTable) - HT_WATCH_OFFSET, watch);
        watch->type = WATCH_ON_HASHTABLE;
+       watch->implicit_ht_count = 0;
 }
 
+static int phpdbg_create_recursive_ht_watch(phpdbg_watchpoint_t *watch);
+static int phpdbg_create_recursive_zval_watch(phpdbg_watchpoint_t *watch);
+
 void phpdbg_watch_HashTable_dtor(zval *ptr);
 
+static void phpdbg_free_watch(phpdbg_watchpoint_t *watch) {
+       zend_string_release(watch->str);
+       zend_string_release(watch->name_in_parent);     
+}
+
 static int phpdbg_delete_watchpoint(phpdbg_watchpoint_t *tmp_watch);
 static void phpdbg_delete_ht_watchpoints_recursive(phpdbg_watchpoint_t *watch);
 static void phpdbg_delete_zval_watchpoints_recursive(phpdbg_watchpoint_t *watch);
 static void phpdbg_delete_watchpoints_recursive(phpdbg_watchpoint_t *watch);
 
-/* TODO: Store all the possible watches the refcounted may refer to (for displaying & deleting by identifier) */
-
+/* Store all the possible watches the refcounted may refer to (for displaying & deleting by identifier) [collision] */
 static phpdbg_watchpoint_t *phpdbg_create_refcounted_watchpoint(phpdbg_watchpoint_t *parent, zend_refcounted *ref) {
        phpdbg_watchpoint_t *watch = emalloc(sizeof(phpdbg_watchpoint_t));
        watch->flags = parent->flags;
        watch->parent = parent;
+       watch->str = parent->str;
+       ++GC_REFCOUNT(parent->str);
        phpdbg_create_addr_watchpoint(&ref->refcount, sizeof(uint32_t), watch);
        watch->type = WATCH_ON_REFCOUNTED;
 
        return watch;
 }
 
+/* Must prevent duplicates ... if there are duplicates, replace new by old! */
 static void phpdbg_add_watch_collision(phpdbg_watchpoint_t *watch) {
        phpdbg_watch_collision *cur;
+
+       /* Check for either recursive or (simple and/or implicit) */
+       ZEND_ASSERT(((watch->flags & PHPDBG_WATCH_RECURSIVE) == 0) ^ ((watch->flags & (PHPDBG_WATCH_IMPLICIT | PHPDBG_WATCH_SIMPLE)) == 0));
+
        if ((cur = zend_hash_index_find_ptr(&PHPDBG_G(watch_collisions), (zend_ulong) watch->addr.ref))) {
-               cur->num++;
-               if (watch->flags == PHPDBG_WATCH_RECURSIVE) {
-                       cur->refs++;
+               phpdbg_watchpoint_t *old;
+               int flags = 0;
+               if ((old = zend_hash_find_ptr(&cur->watches, watch->str)) || (old = zend_hash_find_ptr(&cur->implicit_watches, watch->str))) {
+                       if (((old->flags ^ watch->flags) & (PHPDBG_WATCH_NORMAL|PHPDBG_WATCH_IMPLICIT)) == 0) {
+                               return; /* there was no change ... */
+                       }
+
+                       flags = old->flags;
+
+                       if (flags & PHPDBG_WATCH_RECURSIVE) {
+                               if (!(watch->flags & PHPDBG_WATCH_RECURSIVE) && !--cur->refs) {
+                                       phpdbg_delete_watchpoints_recursive(watch);
+                               }
+                       }
+                       if (flags & PHPDBG_WATCH_NORMAL) {
+                               zend_hash_del(&cur->watches, watch->str);
+                               if (zend_hash_num_elements(&cur->watches) > 0) {
+                                       cur->watch = Z_PTR_P(zend_hash_get_current_data_ex(&cur->watches, NULL));
+                               } else {
+                                       cur->watch = Z_PTR_P(zend_hash_get_current_data_ex(&cur->implicit_watches, NULL));
+                               }
+                       }
+                       if (flags & PHPDBG_WATCH_IMPLICIT) {
+                               zend_hash_del(&cur->implicit_watches, watch->str);
+                       }
+                       
+                       old->flags = watch->flags;
+                       phpdbg_free_watch(watch);
+                       efree(watch);
+                       watch = old;
+               }
+               if (watch->flags & PHPDBG_WATCH_RECURSIVE) {
+                       if (!(flags & PHPDBG_WATCH_RECURSIVE) && !cur->refs++) {
+                               phpdbg_create_recursive_zval_watch(watch->parent);
+                       }
                }
        } else {
                phpdbg_watch_collision coll;
-               coll.num = 1;
-               coll.refs = watch->flags == PHPDBG_WATCH_RECURSIVE;
-               coll.watch = *watch;
-               zend_hash_init(&coll.watches, 8, NULL, NULL, 0);
+               coll.refs = (watch->flags & PHPDBG_WATCH_RECURSIVE) != 0;
+               coll.watch = watch;
+               zend_hash_init(&coll.watches, 8, arghs, NULL, 0);
+               zend_hash_init(&coll.implicit_watches, 8, ..., NULL, 0);
                cur = zend_hash_index_add_mem(&PHPDBG_G(watch_collisions), (zend_ulong) watch->addr.ref, &coll, sizeof(phpdbg_watch_collision));
-               phpdbg_store_watchpoint(&cur->watch);
-               phpdbg_activate_watchpoint(&cur->watch);
+               phpdbg_store_watchpoint(cur->watch);
+               phpdbg_activate_watchpoint(cur->watch);
+               if (coll.refs) {
+                       phpdbg_create_recursive_zval_watch(watch->parent);
+               }
        }
 
-       zend_hash_add_ptr(&cur->watches, watch->parent->str, watch->parent);
+       if (watch->flags & PHPDBG_WATCH_NORMAL) {
+               cur->watch = watch;
+               zend_hash_add_ptr(&cur->watches, watch->str, watch->parent);
+       }
+       if (watch->flags & PHPDBG_WATCH_IMPLICIT) {
+               zend_hash_add_ptr(&cur->implicit_watches, watch->str, watch->parent);
+       }
 }
 
-static void phpdbg_remove_watch_collision(zend_refcounted *ref) {
+static void phpdbg_remove_watch_collision(phpdbg_watchpoint_t *watch) {
        phpdbg_watch_collision *cur;
-       if ((cur = zend_hash_index_find_ptr(&PHPDBG_G(watch_collisions), (zend_ulong) ref))) {
-               phpdbg_watchpoint_t *watch = cur->watch.parent;
-
-               if (watch->flags == PHPDBG_WATCH_RECURSIVE && !--cur->refs) {
+       if ((cur = zend_hash_index_find_ptr(&PHPDBG_G(watch_collisions), (zend_ulong) Z_COUNTED_P(watch->addr.zv)))) {
+               if (cur->refs && !--cur->refs) {
                        phpdbg_delete_watchpoints_recursive(watch);
                }
 
                zend_hash_del(&cur->watches, watch->str);
+               zend_hash_del(&cur->implicit_watches, watch->str);
 
-               if (!--cur->num) {
-                       phpdbg_deactivate_watchpoint(&cur->watch);
-                       phpdbg_remove_watchpoint(&cur->watch);
-
-                       phpdbg_delete_watchpoint(watch);
+               if (zend_hash_num_elements(&cur->watches) > 0) {
+                       cur->watch = Z_PTR_P(zend_hash_get_current_data_ex(&cur->watches, NULL));
+               } else if (zend_hash_num_elements(&cur->implicit_watches) > 0) {
+                       cur->watch = Z_PTR_P(zend_hash_get_current_data_ex(&cur->implicit_watches, NULL));
+               } else {
+                       phpdbg_deactivate_watchpoint(cur->watch);
+                       phpdbg_remove_watchpoint(cur->watch);
 
-                       zend_hash_index_del(&PHPDBG_G(watch_collisions), (zend_ulong) ref);
+                       zend_hash_index_del(&PHPDBG_G(watch_collisions), (zend_ulong) Z_COUNTED_P(watch->addr.zv));
                }
        }
 }
 
-static void phpdbg_create_reference_watch(phpdbg_watchpoint_t *watch) {
+static phpdbg_watchpoint_t *phpdbg_create_watchpoint(phpdbg_watchpoint_t *watch);
+
+static phpdbg_watchpoint_t *phpdbg_create_reference_watch(phpdbg_watchpoint_t *watch) {
        phpdbg_watchpoint_t *ref = emalloc(sizeof(phpdbg_watchpoint_t));
        watch->reference = ref;
        ref->flags = watch->flags;
@@ -179,15 +293,72 @@ static void phpdbg_create_reference_watch(phpdbg_watchpoint_t *watch) {
        ref->parent_container = NULL;
        phpdbg_create_zval_watchpoint(Z_REFVAL_P(watch->addr.zv), ref);
 
-       phpdbg_store_watchpoint(ref);
-       phpdbg_activate_watchpoint(ref);
+       phpdbg_create_watchpoint(ref);
+
+       return ref;
 }
 
-static int phpdbg_create_watchpoint(phpdbg_watchpoint_t *watch) {
-       watch->flags |= PHPDBG_WATCH_SIMPLE;
+static phpdbg_watchpoint_t *phpdbg_get_refcount_watch(phpdbg_watchpoint_t *parent) {
+       zend_refcounted *ref;
+       phpdbg_btree_result *res;
+
+       if (parent->type == WATCH_ON_HASHTABLE) {
+               parent = parent->parent;
+               if (!parent) {
+                       return NULL;
+               }
+       }
+
+       ZEND_ASSERT(parent->type == WATCH_ON_ZVAL);
+       ref = Z_COUNTED_P(parent->addr.zv);
+
+       res = phpdbg_btree_find(&PHPDBG_G(watchpoint_tree), (zend_ulong) ref);
+       if (res) {
+               return res->ptr;
+       }
+       return NULL;
+}
+
+static phpdbg_watchpoint_t *phpdbg_create_watchpoint(phpdbg_watchpoint_t *watch) {
+       phpdbg_watchpoint_t *ret = watch;
+
+       /* exclude references & refcounted */
+       if (!watch->parent || watch->parent->type != WATCH_ON_ZVAL || watch->type == WATCH_ON_HASHTABLE) {
+               phpdbg_watchpoint_t *old_watch = zend_hash_find_ptr(&PHPDBG_G(watchpoints), watch->str);
+
+               if (old_watch) {
+#define return_and_free_watch(x) { \
+       phpdbg_watchpoint_t *ref = phpdbg_get_refcount_watch(old_watch); \
+       if (ref) { \
+               phpdbg_add_watch_collision(ref); \
+       } \
+       phpdbg_free_watch(watch); \
+       efree(watch); \
+       return (x); \
+}
+                       if (watch->flags & PHPDBG_WATCH_RECURSIVE) {
+                               if (old_watch->flags & PHPDBG_WATCH_RECURSIVE) {
+                                       return_and_free_watch(NULL);
+                               } else {
+                                       old_watch->flags &= ~(PHPDBG_WATCH_SIMPLE | PHPDBG_WATCH_IMPLICIT);
+                                       old_watch->flags |= PHPDBG_WATCH_RECURSIVE;
+                                       return_and_free_watch(old_watch);
+                               }
+                       } else {
+                               if (!(old_watch->flags & PHPDBG_WATCH_RECURSIVE)) {
+                                       old_watch->flags |= watch->flags & (PHPDBG_WATCH_IMPLICIT | PHPDBG_WATCH_SIMPLE);
+                               }
+                               return_and_free_watch(NULL);
+                       }
+               } else {
+                       if (watch->parent && watch->parent->type == WATCH_ON_HASHTABLE) {
+                               watch->parent->implicit_ht_count++;
+                       }
+                       zend_hash_add_ptr(&PHPDBG_G(watchpoints), watch->str, watch);
+               }
+       }
 
        phpdbg_store_watchpoint(watch);
-       zend_hash_add_ptr(&PHPDBG_G(watchpoints), watch->str, watch);
 
        if (watch->parent && watch->parent->type == WATCH_ON_ZVAL && Z_REFCOUNTED_P(watch->parent->addr.zv)) {
                phpdbg_add_watch_collision(phpdbg_create_refcounted_watchpoint(watch, Z_COUNTED_P(watch->parent->addr.zv)));
@@ -195,17 +366,36 @@ static int phpdbg_create_watchpoint(phpdbg_watchpoint_t *watch) {
 
        if (watch->type == WATCH_ON_ZVAL) {
                if (watch->parent_container) {
-                       phpdbg_btree_insert(&PHPDBG_G(watch_HashTables), (zend_ulong) watch->parent_container, watch->parent_container->pDestructor);
-                       watch->parent_container->pDestructor = (dtor_func_t) phpdbg_watch_HashTable_dtor;
+                       HashTable *ht_watches;
+                       phpdbg_btree_result *find;
+                       if (!(find = phpdbg_btree_find(&PHPDBG_G(watch_HashTables), (zend_ulong) watch->parent_container))) {
+                               phpdbg_watch_ht_info *hti = emalloc(sizeof(*hti));
+                               hti->dtor = watch->parent_container->pDestructor;
+                               ht_watches = &hti->watches;
+                               zend_hash_init(ht_watches, 0, grrrrr, ZVAL_PTR_DTOR, 0);
+                               phpdbg_btree_insert(&PHPDBG_G(watch_HashTables), (zend_ulong) watch->parent_container, hti);
+                               watch->parent_container->pDestructor = (dtor_func_t) phpdbg_watch_HashTable_dtor;
+                       } else {
+                               ht_watches = &((phpdbg_watch_ht_info *) find->ptr)->watches;
+                       }
+                       zend_hash_add_ptr(ht_watches, watch->name_in_parent, watch);
                }
 
                if (Z_ISREF_P(watch->addr.zv)) {
-                       phpdbg_create_reference_watch(watch);
+                       ret = phpdbg_create_reference_watch(watch);
                }
        }
 
        phpdbg_activate_watchpoint(watch);
 
+       return ret;
+}
+
+static int phpdbg_create_simple_watchpoint(phpdbg_watchpoint_t *watch) {
+       watch->flags |= PHPDBG_WATCH_SIMPLE;
+
+       phpdbg_create_watchpoint(watch);
+
        return SUCCESS;
 }
 
@@ -222,7 +412,9 @@ static int phpdbg_create_array_watchpoint(phpdbg_watchpoint_t *zv_watch) {
 
        phpdbg_create_ht_watchpoint(ht, watch);
 
-       phpdbg_create_watchpoint(watch);
+       if (phpdbg_create_watchpoint(watch) == NULL) {
+               return SUCCESS;
+       }
 
        if (Z_TYPE_P(zv) == IS_ARRAY) {
                watch->flags |= PHPDBG_WATCH_ARRAY;
@@ -236,59 +428,68 @@ static int phpdbg_create_array_watchpoint(phpdbg_watchpoint_t *zv_watch) {
 }
 
 static int phpdbg_create_recursive_watchpoint(phpdbg_watchpoint_t *watch) {
-       size_t str_len;
-       HashTable *ht;
-       zval *zvp = watch->addr.zv;
-
        if (watch->type != WATCH_ON_ZVAL) {
                return FAILURE;
        }
 
        watch->flags |= PHPDBG_WATCH_RECURSIVE;
-       phpdbg_create_watchpoint(watch);
-
-       ZVAL_DEREF(zvp);
+       watch = phpdbg_create_watchpoint(watch);
 
-       if (!(ht = HT_FROM_ZVP(zvp))) {
-               return SUCCESS;
-       }
+       return SUCCESS;
+}
 
-       {
-               zval *zv;
-               zend_string *key;
-               zend_long h;
+static int phpdbg_create_recursive_ht_watch(phpdbg_watchpoint_t *watch) {
+       zval *zv;
+       zend_string *key;
+       zend_long h;
+       size_t str_len;
 
-               ZEND_HASH_FOREACH_KEY_VAL(ht, h, key, zv) {
-                       char *str = NULL;
-                       phpdbg_watchpoint_t *new_watch = emalloc(sizeof(phpdbg_watchpoint_t));
+       ZEND_ASSERT(watch->type == WATCH_ON_HASHTABLE);
 
-                       new_watch->flags = PHPDBG_WATCH_RECURSIVE;
-                       new_watch->parent = watch;
-                       new_watch->parent_container = ht;
+       ZEND_HASH_FOREACH_KEY_VAL(HT_WATCH_HT(watch), h, key, zv) {
+               char *str = NULL;
+               phpdbg_watchpoint_t *new_watch = emalloc(sizeof(phpdbg_watchpoint_t));
 
-                       if (key) {
-                               new_watch->name_in_parent = key;
-                               ++GC_REFCOUNT(key);
-                       } else {
-                               str_len = spprintf(&str, 0, "%lld", h);
-                               new_watch->name_in_parent = zend_string_init(str, str_len, 0);
-                               efree(str);
-                       }
+               new_watch->flags = PHPDBG_WATCH_RECURSIVE;
+               new_watch->parent = watch;
+               new_watch->parent_container = HT_WATCH_HT(watch);
 
-                       str_len = spprintf(&str, 0, "%.*s%s%s%s", (int) watch->str->len, watch->str->val, Z_TYPE_P(zvp) == IS_ARRAY ? "[" : "->", phpdbg_get_property_key(new_watch->name_in_parent->val), Z_TYPE_P(zvp) == IS_ARRAY ? "]" : "");
-                       new_watch->str = zend_string_init(str, str_len, 0);
+               if (key) {
+                       new_watch->name_in_parent = key;
+                       ++GC_REFCOUNT(key);
+               } else {
+                       str_len = spprintf(&str, 0, "%lld", h);
+                       new_watch->name_in_parent = zend_string_init(str, str_len, 0);
                        efree(str);
+               }
 
-                       while (Z_TYPE_P(zv) == IS_INDIRECT) {
-                               zv = Z_INDIRECT_P(zv);
-                       }
+               str_len = spprintf(&str, 0, "%.*s%s%s%s", (int) watch->str->len - 2, watch->str->val, (watch->flags & PHPDBG_WATCH_ARRAY) ? "[" : "->", phpdbg_get_property_key(new_watch->name_in_parent->val), (watch->flags & PHPDBG_WATCH_ARRAY) ? "]" : "");
+               new_watch->str = zend_string_init(str, str_len, 0);
+               efree(str);
 
-                       phpdbg_create_zval_watchpoint(zv, new_watch);
-                       phpdbg_create_recursive_watchpoint(new_watch);
-               } ZEND_HASH_FOREACH_END();
-       }
+               while (Z_TYPE_P(zv) == IS_INDIRECT) {
+                       zv = Z_INDIRECT_P(zv);
+               }
+
+               phpdbg_create_zval_watchpoint(zv, new_watch);
+               new_watch = phpdbg_create_watchpoint(new_watch);
+               phpdbg_create_recursive_zval_watch(new_watch);
+       } ZEND_HASH_FOREACH_END();
+
+       return SUCCESS;
+}
+
+static int phpdbg_create_recursive_zval_watch(phpdbg_watchpoint_t *watch) {
+       size_t str_len;
+       HashTable *ht;
+       zval *zvp;
+
+       ZEND_ASSERT(watch->type == WATCH_ON_ZVAL);
 
-       {
+       zvp = watch->addr.zv;
+       ZVAL_DEREF(zvp);
+
+       if ((ht = HT_FROM_ZVP(zvp))) {
                char *str = NULL;
                phpdbg_watchpoint_t *new_watch = emalloc(sizeof(phpdbg_watchpoint_t));
 
@@ -308,20 +509,47 @@ static int phpdbg_create_recursive_watchpoint(phpdbg_watchpoint_t *watch) {
                }
 
                phpdbg_create_ht_watchpoint(ht, new_watch);
+
+               phpdbg_create_recursive_ht_watch(new_watch);
+
                phpdbg_create_watchpoint(new_watch);
+
        }
 
        return SUCCESS;
 }
 
+static void phpdbg_delete_implicit_parents(phpdbg_watchpoint_t *watch) {
+       phpdbg_watchpoint_t *parent = watch->parent;
+       if (!parent) {
+               return;
+       }
+
+       ZEND_ASSERT(!(parent->flags & PHPDBG_WATCH_RECURSIVE));
+       ZEND_ASSERT(parent->flags & PHPDBG_WATCH_IMPLICIT);
+
+       if (parent->type == WATCH_ON_HASHTABLE && --parent->implicit_ht_count) {
+               return;
+       }
+
+       parent->flags &= ~PHPDBG_WATCH_IMPLICIT;
+       if (!(parent->flags & PHPDBG_WATCH_SIMPLE)) {
+               if (parent->type == WATCH_ON_ZVAL && Z_REFCOUNTED_P(watch->addr.zv)) {
+                       phpdbg_remove_watch_collision(parent);
+               }
+               zend_hash_del(&PHPDBG_G(watchpoints), parent->str);
+       }
+}
+
+/* delete watchpoint, recursively (and inclusively) */
 static int phpdbg_delete_watchpoint_recursive(phpdbg_watchpoint_t *watch, zend_bool user_request) {
        if (watch->type == WATCH_ON_HASHTABLE) {
-               HashTable *ht;
-               phpdbg_btree_result *result;
-
-               if (watch->type == WATCH_ON_HASHTABLE && user_request) {
+               if (user_request) {
                        phpdbg_delete_ht_watchpoints_recursive(watch);
                } else {
+                       HashTable *ht;
+                       phpdbg_btree_result *result;
+
                        ht = HT_FROM_ZVP(watch->addr.zv);
 
                        if ((result = phpdbg_btree_find(&PHPDBG_G(watchpoint_tree), (zend_ulong) ht))) {
@@ -342,7 +570,7 @@ static void phpdbg_delete_ht_watchpoints_recursive(phpdbg_watchpoint_t *watch) {
        size_t str_len;
        phpdbg_watchpoint_t *watchpoint;
 
-       ZEND_HASH_FOREACH_KEY(watch->addr.ht, numkey, strkey) {
+       ZEND_HASH_FOREACH_KEY(HT_WATCH_HT(watch), numkey, strkey) {
                if (strkey) {
                        str_len = spprintf(&str, 0, "%.*s%s%s%s", (int) watch->str->len, watch->str->val, (watch->flags & PHPDBG_WATCH_ARRAY) ? "[" : "->", phpdbg_get_property_key(strkey->val), (watch->flags & PHPDBG_WATCH_ARRAY) ? "]" : "");
                } else {
@@ -359,10 +587,11 @@ static void phpdbg_delete_ht_watchpoints_recursive(phpdbg_watchpoint_t *watch) {
 
 static void phpdbg_delete_zval_watchpoints_recursive(phpdbg_watchpoint_t *watch) {
        if (Z_REFCOUNTED_P(watch->addr.zv)) {
-               phpdbg_remove_watch_collision(Z_COUNTED_P(watch->addr.zv));
+               phpdbg_remove_watch_collision(watch);
        }
 }
 
+/* delete watchpoints recusively (exclusively!) */
 static void phpdbg_delete_watchpoints_recursive(phpdbg_watchpoint_t *watch) {
        if (watch->type == WATCH_ON_ZVAL) {
                phpdbg_delete_zval_watchpoints_recursive(watch);
@@ -382,14 +611,17 @@ static int phpdbg_delete_watchpoint(phpdbg_watchpoint_t *tmp_watch) {
 
        watch = result->ptr;
 
+       if (!(watch->flags & PHPDBG_WATCH_NORMAL) || (watch->parent && (watch->parent->flags & PHPDBG_WATCH_RECURSIVE))) {
+               return FAILURE; /* TODO: better error message for recursive (??) */
+       }
+
        if (watch->flags & PHPDBG_WATCH_RECURSIVE) {
                ret = phpdbg_delete_watchpoint_recursive(watch, 1);
        } else {
                ret = zend_hash_del(&PHPDBG_G(watchpoints), watch->str);
        }
 
-       zend_string_release(tmp_watch->str);
-       zend_string_release(tmp_watch->name_in_parent);
+       phpdbg_free_watch(tmp_watch);
        efree(tmp_watch);
 
        return ret;
@@ -398,7 +630,6 @@ static int phpdbg_delete_watchpoint(phpdbg_watchpoint_t *tmp_watch) {
 static int phpdbg_watchpoint_parse_wrapper(char *name, size_t namelen, char *key, size_t keylen, HashTable *parent, zval *zv, int (*callback)(phpdbg_watchpoint_t *)) {
        int ret;
        phpdbg_watchpoint_t *watch = ecalloc(1, sizeof(phpdbg_watchpoint_t));
-       watch->flags = 0;
        watch->str = zend_string_init(name, namelen, 0);
        watch->name_in_parent = zend_string_init(key, keylen, 0);
        watch->parent_container = parent;
@@ -410,14 +641,43 @@ static int phpdbg_watchpoint_parse_wrapper(char *name, size_t namelen, char *key
        efree(key);
 
        if (ret != SUCCESS) {
+               phpdbg_free_watch(watch);
                efree(watch);
        }
 
+       PHPDBG_G(watch_tmp) = NULL;
+
        return ret;
 }
 
 PHPDBG_API int phpdbg_watchpoint_parse_input(char *input, size_t len, HashTable *parent, size_t i, int (*callback)(phpdbg_watchpoint_t *), zend_bool silent) {
-       return phpdbg_parse_variable_with_arg(input, len, parent, i, (phpdbg_parse_var_with_arg_func) phpdbg_watchpoint_parse_wrapper, 0, callback);
+       return phpdbg_parse_variable_with_arg(input, len, parent, i, (phpdbg_parse_var_with_arg_func) phpdbg_watchpoint_parse_wrapper, NULL, 0, callback);
+}
+
+static int phpdbg_watchpoint_parse_step(char *name, size_t namelen, char *key, size_t keylen, HashTable *parent, zval *zv, int (*callback)(phpdbg_watchpoint_t *)) {
+       phpdbg_watchpoint_t *watch;
+
+       if ((watch = zend_hash_str_find_ptr(&PHPDBG_G(watchpoints), name, namelen))) {
+               watch->flags |= PHPDBG_WATCH_IMPLICIT;
+               PHPDBG_G(watch_tmp) = watch;
+               return SUCCESS;
+       }
+
+       watch = ecalloc(1, sizeof(phpdbg_watchpoint_t));
+       watch->flags = PHPDBG_WATCH_IMPLICIT;
+       watch->str = zend_string_init(name, namelen, 0);
+       watch->name_in_parent = zend_string_init(key, keylen, 0);
+       watch->parent_container = parent;
+       watch->parent = PHPDBG_G(watch_tmp);
+       phpdbg_create_zval_watchpoint(zv, watch);
+
+       phpdbg_create_watchpoint(watch);
+
+       efree(name);
+       efree(key);
+
+       PHPDBG_G(watch_tmp) = watch;
+       return SUCCESS;
 }
 
 static int phpdbg_watchpoint_parse_symtables(char *input, size_t len, int (*callback)(phpdbg_watchpoint_t *)) {
@@ -429,7 +689,7 @@ static int phpdbg_watchpoint_parse_symtables(char *input, size_t len, int (*call
                return SUCCESS;
        }
 
-       return phpdbg_watchpoint_parse_input(input, len, EG(current_execute_data)->symbol_table, 0, callback, 0);
+       return phpdbg_parse_variable_with_arg(input, len, EG(current_execute_data)->symbol_table, 0, (phpdbg_parse_var_with_arg_func) phpdbg_watchpoint_parse_wrapper, (phpdbg_parse_var_with_arg_func) phpdbg_watchpoint_parse_step, 0, callback);
 }
 
 PHPDBG_WATCH(delete) /* {{{ */
@@ -489,8 +749,7 @@ PHPDBG_WATCH(array) /* {{{ */
 
 void phpdbg_watch_HashTable_dtor(zval *zv) {
        phpdbg_btree_result *result;
-
-       zval_ptr_dtor_wrapper(zv);
+       zval *orig_zv = zv;
 
        while (Z_TYPE_P(zv) == IS_INDIRECT) {
                zv = Z_INDIRECT_P(zv);
@@ -499,25 +758,39 @@ void phpdbg_watch_HashTable_dtor(zval *zv) {
        if ((result = phpdbg_btree_find(&PHPDBG_G(watchpoint_tree), (zend_ulong) zv))) {
                phpdbg_watchpoint_t *watch = result->ptr;
 
-               PHPDBG_G(watchpoint_hit) = 1;
+               if (watch->flags & PHPDBG_WATCH_NORMAL) {
+                       PHPDBG_G(watchpoint_hit) = 1;
 
-               phpdbg_notice("watchdelete", "variable=\"%.*s\" recursive=\"%s\"", "%.*s was removed, removing watchpoint%s", (int) watch->str->len, watch->str->val, (watch->flags & PHPDBG_WATCH_RECURSIVE) ? " recursively" : "");
+                       phpdbg_notice("watchdelete", "variable=\"%.*s\" recursive=\"%s\"", "%.*s was removed, removing watchpoint%s", (int) watch->str->len, watch->str->val, (watch->flags & PHPDBG_WATCH_RECURSIVE) ? " recursively" : "");
+               }
 
                if (watch->flags & PHPDBG_WATCH_RECURSIVE) {
                        phpdbg_delete_watchpoint_recursive(watch, 0);
                } else {
                        zend_hash_del(&PHPDBG_G(watchpoints), watch->str);
                }
+
+               if ((result = phpdbg_btree_find(&PHPDBG_G(watch_HashTables), (zend_ulong) watch->parent_container))) {
+                       phpdbg_watch_ht_info *hti = result->ptr;
+                       hti->dtor(orig_zv);
+                       zend_hash_del(&hti->watches, watch->name_in_parent);
+                       if (zend_hash_num_elements(&hti->watches) == 0) {
+                               watch->parent_container->pDestructor = hti->dtor;
+                               zend_hash_destroy(&hti->watches);
+                               efree(hti);
+                       }
+               } else {
+                       zval_ptr_dtor_wrapper(orig_zv);
+               }
        }
 }
 
-
 int phpdbg_create_var_watchpoint(char *input, size_t len) {
        if (phpdbg_rebuild_symtable() == FAILURE) {
                return FAILURE;
        }
 
-       return phpdbg_watchpoint_parse_symtables(input, len, phpdbg_create_watchpoint);
+       return phpdbg_watchpoint_parse_symtables(input, len, phpdbg_create_simple_watchpoint);
 }
 
 int phpdbg_delete_var_watchpoint(char *input, size_t len) {
@@ -525,7 +798,7 @@ int phpdbg_delete_var_watchpoint(char *input, size_t len) {
                return FAILURE;
        }
 
-       return phpdbg_watchpoint_parse_symtables(input, len, phpdbg_delete_watchpoint);
+       return phpdbg_watchpoint_parse_input(input, len, EG(current_execute_data)->symbol_table, 0, phpdbg_delete_watchpoint, 0);
 }
 
 #ifdef _WIN32
@@ -571,14 +844,32 @@ void phpdbg_watchpoints_clean(void) {
        zend_hash_clean(&PHPDBG_G(watchpoints));
 }
 
+/* due to implicit delete... MUST BE DESTROYED MANUALLY */
 static void phpdbg_watch_dtor(zval *pDest) {
        phpdbg_watchpoint_t *watch = (phpdbg_watchpoint_t *) Z_PTR_P(pDest);
 
+       if (watch->flags & PHPDBG_WATCH_IMPLICIT) {
+               watch->flags = PHPDBG_WATCH_SIMPLE; // tiny hack for delete_implicit_parents
+
+               if (watch->type == WATCH_ON_ZVAL) {
+                       phpdbg_delete_zval_watchpoints_recursive(watch);
+               } else if (watch->type == WATCH_ON_HASHTABLE) {
+                       phpdbg_watchpoint_t *watchpoint;
+
+                       watch->implicit_ht_count++;
+
+                       ZEND_HASH_FOREACH_PTR(&((phpdbg_watch_ht_info *) phpdbg_btree_find(&PHPDBG_G(watch_HashTables), (zend_ulong) HT_WATCH_HT(watch))->ptr)->watches, watchpoint) {
+                               phpdbg_delete_watchpoint_recursive(watchpoint, 1);
+                       } ZEND_HASH_FOREACH_END();
+               }
+       }
+
+       phpdbg_delete_implicit_parents(watch);
+
        phpdbg_deactivate_watchpoint(watch);
        phpdbg_remove_watchpoint(watch);
 
-       zend_string_release(watch->str);
-       zend_string_release(watch->name_in_parent);
+       phpdbg_free_watch(watch);
 }
 
 static void phpdbg_watch_mem_dtor(void *llist_data) {
@@ -592,6 +883,10 @@ static void phpdbg_watch_mem_dtor(void *llist_data) {
        free(dump);
 }
 
+static void phpdbg_watch_free_ptr_dtor(zval *ptr) {
+       efree(Z_PTR_P(ptr));
+}
+
 void phpdbg_setup_watchpoints(void) {
 #if _SC_PAGE_SIZE
        phpdbg_pagesize = sysconf(_SC_PAGE_SIZE);
@@ -607,7 +902,7 @@ void phpdbg_setup_watchpoints(void) {
        phpdbg_btree_init(&PHPDBG_G(watchpoint_tree), sizeof(void *) * 8);
        phpdbg_btree_init(&PHPDBG_G(watch_HashTables), sizeof(void *) * 8);
        zend_hash_init(&PHPDBG_G(watchpoints), 8, NULL, phpdbg_watch_dtor, 0);
-       zend_hash_init(&PHPDBG_G(watch_collisions), 8, NULL, NULL, 0);
+       zend_hash_init(&PHPDBG_G(watch_collisions), 8, NULL, phpdbg_watch_free_ptr_dtor, 0);
 }
 
 static void phpdbg_print_changed_zval(phpdbg_watch_memdump *dump) {
@@ -665,21 +960,22 @@ static void phpdbg_print_changed_zval(phpdbg_watch_memdump *dump) {
                if (memcmp(oldPtr, watch->addr.ptr, watch->size) != SUCCESS) {
                        zend_bool do_break = 0;
 
-                       switch (watch->type) {
-                               case WATCH_ON_ZVAL:
-                                       do_break = memcmp(oldPtr, watch->addr.zv, sizeof(zend_value) + sizeof(uint32_t) /* value + typeinfo */);
-                                       break;
-                               case WATCH_ON_HASHTABLE:
-                                       do_break = zend_hash_num_elements((HashTable *) oldPtr) != zend_hash_num_elements(watch->addr.ht);
-                                       break;
-                               case WATCH_ON_REFCOUNTED:
-                                       if (PHPDBG_G(flags) & PHPDBG_SHOW_REFCOUNTS) {
-                                               do_break = memcmp(oldPtr, watch->addr.ref, sizeof(uint32_t) /* no zend_refcounted metadata info */);
-                                       }
-                                       break;
+                       if (watch->flags & PHPDBG_WATCH_NORMAL) {
+                               switch (watch->type) {
+                                       case WATCH_ON_ZVAL:
+                                               do_break = memcmp(oldPtr, watch->addr.zv, sizeof(zend_value) + sizeof(uint32_t) /* value + typeinfo */);
+                                               break;
+                                       case WATCH_ON_HASHTABLE:
+                                               do_break = zend_hash_num_elements(HT_PTR_HT(oldPtr)) != zend_hash_num_elements(HT_WATCH_HT(watch));
+                                               break;
+                                       case WATCH_ON_REFCOUNTED:
+                                               if (PHPDBG_G(flags) & PHPDBG_SHOW_REFCOUNTS) {
+                                                       do_break = memcmp(oldPtr, watch->addr.ref, sizeof(uint32_t) /* no zend_refcounted metadata info */);
+                                               }
+                                               break;
+                               }
                        }
 
-
                        if (do_break) {
                                PHPDBG_G(watchpoint_hit) = 1;
 
@@ -691,9 +987,10 @@ static void phpdbg_print_changed_zval(phpdbg_watch_memdump *dump) {
                                case WATCH_ON_ZVAL: {
                                        int show_value = memcmp(oldPtr, watch->addr.zv, sizeof(zend_value) + sizeof(uint32_t) /* no metadata info */);
 
-                                       if (removed || show_value) {
-                                               if (removed && (Z_TYPE_P((zval *) oldPtr) == IS_ARRAY || Z_TYPE_P((zval *) oldPtr) == IS_OBJECT)) {
-                                                       phpdbg_writeln("watchvalue", "type=\"old\" inaccessible=\"inaccessible\"", "Old value inaccessible, array or object (HashTable) already destroyed");
+                                       if ((watch->flags & PHPDBG_WATCH_NORMAL) && (removed || show_value)) {
+/* TODO: Merge with refcounting watches, store if watched ref value is to be dropped etc. [for example: manually increment refcount transparently for displaying and drop it if it decrements to 1] */
+                                               if (Z_REFCOUNTED_P((zval *) oldPtr)) {
+                                                       phpdbg_writeln("watchvalue", "type=\"old\" inaccessible=\"inaccessible\"", "Old value inaccessible or destroyed");
                                                } else {
                                                        phpdbg_out("Old value: ");
                                                        phpdbg_xml("<watchvalue %r type=\"old\">");
@@ -705,18 +1002,20 @@ static void phpdbg_print_changed_zval(phpdbg_watch_memdump *dump) {
 
                                        /* check if zval was removed */
                                        if (removed) {
-                                               phpdbg_notice("watchdelete", "variable=\"%.*s\"", "Watchpoint %.*s was unset, removing watchpoint", (int) watch->str->len, watch->str->val);
+                                               if (watch->flags & PHPDBG_WATCH_NORMAL) {
+                                                       phpdbg_notice("watchdelete", "variable=\"%.*s\"", "Watchpoint %.*s was unset, removing watchpoint", (int) watch->str->len, watch->str->val);
+                                               }
                                                zend_hash_del(&PHPDBG_G(watchpoints), watch->str);
 
                                                reenable = 0;
 
                                                if (Z_REFCOUNTED_P((zval *) oldPtr)) {
-                                                       phpdbg_remove_watch_collision(Z_COUNTED_P((zval *) oldPtr));
+                                                       phpdbg_remove_watch_collision(watch);
                                                }
                                                break;
                                        }
 
-                                       if (show_value) {
+                                       if ((watch->flags & PHPDBG_WATCH_NORMAL) && show_value) {
                                                phpdbg_out("New value%s: ", Z_ISREF_P(watch->addr.zv) ? " (reference)" : "");
                                                phpdbg_xml("<watchvalue %r%s type=\"new\">", Z_ISREF_P(watch->addr.zv) ? " reference=\"reference\"" : "");
                                                zend_print_flat_zval_r(watch->addr.zv);
@@ -727,10 +1026,13 @@ static void phpdbg_print_changed_zval(phpdbg_watch_memdump *dump) {
                                        /* add new watchpoints if necessary */
                                        if (Z_PTR_P(watch->addr.zv) != Z_PTR_P((zval *) oldPtr) || Z_TYPE_P(watch->addr.zv) != Z_TYPE_P((zval *) oldPtr)) {
                                                if (Z_REFCOUNTED_P((zval *) oldPtr)) {
-                                                       phpdbg_remove_watch_collision(Z_COUNTED_P((zval *) oldPtr));
+                                                       zval *new_zv = watch->addr.zv;
+                                                       watch->addr.ptr = oldPtr;
+                                                       phpdbg_remove_watch_collision(watch);
+                                                       watch->addr.zv = new_zv;
                                                }
                                                if (Z_REFCOUNTED_P(watch->addr.zv)) {
-                                                       if (PHPDBG_G(flags) & PHPDBG_SHOW_REFCOUNTS) {
+                                                       if ((watch->flags & PHPDBG_WATCH_NORMAL) && (PHPDBG_G(flags) & PHPDBG_SHOW_REFCOUNTS)) {
                                                                phpdbg_writeln("watchrefcount", "type=\"new\" refcount=\"%d\"", "New refcount: %d", Z_COUNTED_P(watch->addr.zv)->refcount);
                                                        }
                                                        if (watch->flags & PHPDBG_WATCH_RECURSIVE) {
@@ -744,42 +1046,28 @@ static void phpdbg_print_changed_zval(phpdbg_watch_memdump *dump) {
                                        break;
                                }
                                case WATCH_ON_HASHTABLE:
-#if 0 && ZEND_DEBUG
-                                       if (watch->addr.arr->ht->inconsistent) {
-                                               phpdbg_notice("watchdelete", "variable=\"%.*s\"", "Watchpoint %.*s was unset, removing watchpoint", (int) watch->str->len, watch->str->val);
-                                               zend_hash_del(&PHPDBG_G(watchpoints), watch->str);
-
-                                               reenable = 0;
-
-                                               break;
-                                       }
-#endif
-                                       elementDiff = zend_hash_num_elements((HashTable *) oldPtr) - zend_hash_num_elements(watch->addr.ht);
-                                       if (elementDiff) {
+                                       /* We should be safely able to assume the HashTable to be consistent (inconsistent HashTables should have been caught by phpdbg_watch_efree() */
+                                       elementDiff = zend_hash_num_elements(HT_PTR_HT(oldPtr)) - zend_hash_num_elements(HT_WATCH_HT(watch));
+                                       if ((watch->flags & PHPDBG_WATCH_NORMAL) && elementDiff) {
                                                if (elementDiff > 0) {
                                                        phpdbg_writeln("watchsize", "removed=\"%d\"", "%d elements were removed from the array", elementDiff);
                                                } else {
                                                        phpdbg_writeln("watchsize", "added=\"%d\"", "%d elements were added to the array", -elementDiff);
-
-                                                       /* add new watchpoints if necessary */
-                                                       if (watch->flags & PHPDBG_WATCH_RECURSIVE) {
-                                                               phpdbg_create_recursive_watchpoint(watch);
-                                                       }
                                                }
                                        }
-                                       if (watch->addr.ht->nInternalPointer != ((HashTable *) oldPtr)->nInternalPointer) {
+                                       /* add new watchpoints if necessary */
+                                       if (watch->flags & PHPDBG_WATCH_RECURSIVE) {
+                                               phpdbg_create_recursive_ht_watch(watch);
+                                       }
+                                       if ((watch->flags & PHPDBG_WATCH_NORMAL) && HT_WATCH_HT(watch)->nInternalPointer != HT_PTR_HT(oldPtr)->nInternalPointer) {
                                                phpdbg_writeln("watcharrayptr", "", "Internal pointer of array was changed");
                                        }
                                        break;
                                case WATCH_ON_REFCOUNTED: {
-                                       if (PHPDBG_G(flags) & PHPDBG_SHOW_REFCOUNTS) {
+                                       if ((watch->flags & PHPDBG_WATCH_NORMAL) && (PHPDBG_G(flags) & PHPDBG_SHOW_REFCOUNTS)) {
                                                phpdbg_writeln("watchrefcount", "type=\"old\" refcount=\"%d\"", "Old refcount: %d", ((zend_refcounted *) oldPtr)->refcount);
-
-                                               if (!removed) {
-                                                       phpdbg_writeln("watchrefcount", "type=\"old\" refcount=\"%d\"", "Old refcount: %d", ((zend_refcounted *) oldPtr)->refcount);
-                                               }
+                                               phpdbg_writeln("watchrefcount", "type=\"new\" refcount=\"%d\"", "New refcount: %d", watch->addr.ref->refcount);
                                        }
-
                                        break;
                                }
                        }
@@ -822,7 +1110,9 @@ void phpdbg_list_watchpoints(void) {
        phpdbg_xml("<watchlist %r>");
 
        ZEND_HASH_FOREACH_PTR(&PHPDBG_G(watchpoints), watch) {
-               phpdbg_writeln("watchvariable", "variable=\"%.*s\" on=\"%s\" type=\"%s\"", "%.*s (%s, %s)", (int) watch->str->len, watch->str->val, watch->type == WATCH_ON_HASHTABLE ? "array" : watch->type == WATCH_ON_REFCOUNTED ? "refcount" : "variable", watch->flags == PHPDBG_WATCH_RECURSIVE ? "recursive" : "simple");
+               if (watch->flags & PHPDBG_WATCH_NORMAL) {
+                       phpdbg_writeln("watchvariable", "variable=\"%.*s\" on=\"%s\" type=\"%s\"", "%.*s (%s, %s)", (int) watch->str->len, watch->str->val, watch->type == WATCH_ON_HASHTABLE ? "array" : watch->type == WATCH_ON_REFCOUNTED ? "refcount" : "variable", watch->flags == PHPDBG_WATCH_RECURSIVE ? "recursive" : "simple");
+               }
        } ZEND_HASH_FOREACH_END();
 
        phpdbg_xml("</watchlist>");
@@ -837,11 +1127,21 @@ void phpdbg_watch_efree(void *ptr) {
                phpdbg_watchpoint_t *watch = result->ptr;
 
                if ((size_t) watch->addr.ptr + watch->size > (size_t) ptr) {
-                       if (watch->type == WATCH_ON_ZVAL) {
-                               phpdbg_remove_watch_collision(Z_COUNTED_P(watch->addr.zv));
-                       }
-                       if (watch->parent == NULL || watch->parent->reference != watch) {
-                               zend_hash_del(&PHPDBG_G(watchpoints), watch->str);
+                       if (watch->type == WATCH_ON_REFCOUNTED) {
+                               /* remove watchpoint here from btree, zval watchpoint will remove it via remove_watch_collison */
+                               phpdbg_deactivate_watchpoint(watch);
+                               phpdbg_remove_watchpoint(watch);
+                       } else {
+                               if (watch->type == WATCH_ON_ZVAL) {
+                                       phpdbg_remove_watch_collision(watch);
+                               }
+                               if (watch->type == WATCH_ON_HASHTABLE && (watch->flags & PHPDBG_WATCH_SIMPLE)) {
+                                       /* when a HashTable is freed, we can safely assume the other zvals all were dtor'ed */
+                                       phpdbg_notice("watchdelete", "variable=\"%.*s\" recursive=\"%s\"", "Array %.*s was removed, removing watchpoint%s", (int) watch->str->len, watch->str->val, (watch->flags & PHPDBG_WATCH_RECURSIVE) ? " recursively" : "");
+                               }
+                               if (watch->type == WATCH_ON_HASHTABLE || watch->parent == NULL || watch->parent->type != WATCH_ON_ZVAL) { /* no references */
+                                       zend_hash_del(&PHPDBG_G(watchpoints), watch->str);
+                               }
                        }
                }
        }
index d101ab116d1c8bc8c77e7687d997b21bacc686dc..ca56af8c3e028e812dcd0a440d266ef7acd82df7 100644 (file)
@@ -48,23 +48,27 @@ typedef enum {
 } phpdbg_watchtype;
 
 
-#define PHPDBG_WATCH_SIMPLE     0x0
-#define PHPDBG_WATCH_RECURSIVE  0x1
-#define PHPDBG_WATCH_ARRAY      0x2
-#define PHPDBG_WATCH_OBJECT     0x4
+#define PHPDBG_WATCH_SIMPLE     0x01
+#define PHPDBG_WATCH_RECURSIVE  0x02
+#define PHPDBG_WATCH_ARRAY      0x04
+#define PHPDBG_WATCH_OBJECT     0x08
+#define PHPDBG_WATCH_NORMAL     (PHPDBG_WATCH_SIMPLE | PHPDBG_WATCH_RECURSIVE)
+#define PHPDBG_WATCH_IMPLICIT   0x10
+
+#define PHPDBG_DESTRUCTED_ZVAL 0x80
 
 typedef struct _phpdbg_watchpoint_t phpdbg_watchpoint_t;
 
 struct _phpdbg_watchpoint_t {
        union {
                zval *zv;
-               HashTable *ht;
                zend_refcounted *ref;
                void *ptr;
        } addr;
        size_t size;
        phpdbg_watchtype type;
        char flags;
+       unsigned int implicit_ht_count;
        phpdbg_watchpoint_t *parent;
        phpdbg_watchpoint_t *reference;
        HashTable *parent_container;
@@ -73,12 +77,17 @@ struct _phpdbg_watchpoint_t {
 };
 
 typedef struct {
-       phpdbg_watchpoint_t watch;
-       unsigned int num;
+       phpdbg_watchpoint_t *watch;
        unsigned int refs;
        HashTable watches;
+       HashTable implicit_watches;
 } phpdbg_watch_collision;
 
+typedef struct {
+       dtor_func_t dtor;
+       HashTable watches;
+} phpdbg_watch_ht_info;
+
 void phpdbg_setup_watchpoints(void);
 
 #ifndef _WIN32