diff options
| -rw-r--r-- | include/osmocom/core/fsm.h | 1 | ||||
| -rw-r--r-- | src/fsm.c | 56 | ||||
| -rw-r--r-- | tests/fsm/fsm_dealloc_test.c | 69 | ||||
| -rw-r--r-- | tests/fsm/fsm_dealloc_test.err | 3408 | 
4 files changed, 3508 insertions, 26 deletions
| diff --git a/include/osmocom/core/fsm.h b/include/osmocom/core/fsm.h index 1701c45e..269befa5 100644 --- a/include/osmocom/core/fsm.h +++ b/include/osmocom/core/fsm.h @@ -122,6 +122,7 @@ struct osmo_fsm_inst {  void osmo_fsm_log_addr(bool log_addr);  void osmo_fsm_log_timeouts(bool log_timeouts);  void osmo_fsm_term_safely(bool term_safely); +void osmo_fsm_set_dealloc_ctx(void *ctx);  /*! Log using FSM instance's context, on explicit logging subsystem and level.   * \param fi  An osmo_fsm_inst. @@ -102,8 +102,20 @@ static __thread struct {  	unsigned int depth;  	/*! Talloc context to collect all deferred deallocations (FSM instances, and talloc objects if any). */  	void *collect_ctx; +	/*! See osmo_fsm_set_dealloc_ctx() */ +	void *fsm_dealloc_ctx;  } fsm_term_safely; +/*! Internal call to free an FSM instance, which redirects to the context set by osmo_fsm_set_dealloc_ctx() if any. + */ +static void fsm_free_or_steal(void *talloc_object) +{ +	if (fsm_term_safely.fsm_dealloc_ctx) +		talloc_steal(fsm_term_safely.fsm_dealloc_ctx, talloc_object); +	else +		talloc_free(talloc_object); +} +  /*! specify if FSM instance addresses should be logged or not   *   *  By default, the FSM name includes the pointer address of the \ref @@ -139,11 +151,9 @@ void osmo_fsm_log_timeouts(bool log_timeouts)  /*! Enable safer way to deallocate cascades of terminating FSM instances.   * - * For legacy compatibility, this is disabled by default. In newer programs / releases, it is recommended to enable this - * feature during main() startup, since it greatly simplifies deallocating child, parent and other FSM instances without - * running into double-free or use-after-free scenarios. When enabled, this feature changes the order of logging, which - * may break legacy unit test expectations, and changes the order of deallocation to after the parent term event is - * dispatched. + * Note, using osmo_fsm_set_dealloc_ctx() is a more general solution to this same problem. + * Particularly, in a program using osmo_select_main_ctx(), the simplest solution to avoid most use-after-free problems + * from FSM instance deallocation is using osmo_fsm_set_dealloc_ctx(OTC_SELECT).   *   * When enabled, an FSM instance termination detects whether another FSM instance is already terminating, and instead of   * deallocating immediately, collects all terminating FSM instances in a talloc context, to be bulk deallocated once all @@ -155,6 +165,9 @@ void osmo_fsm_log_timeouts(bool log_timeouts)   *   * For illustration, see fsm_dealloc_test.c.   * + * When enabled, this feature changes the order of logging, which may break legacy unit test expectations, and changes + * the order of deallocation to after the parent term event is dispatched. + *   * \param[in] term_safely  Pass true to switch to safer FSM instance termination behavior.   */  void osmo_fsm_term_safely(bool term_safely) @@ -162,6 +175,31 @@ void osmo_fsm_term_safely(bool term_safely)  	fsm_term_safely_enabled = term_safely;  } +/*! Instead of deallocating FSM instances, move them to the given talloc context. + * + * It is the caller's responsibility to clear this context to actually free the memory of terminated FSM instances. + * Make sure to not talloc_free(ctx) itself before setting a different osmo_fsm_set_dealloc_ctx(). To clear a ctx + * without the need to call osmo_fsm_set_dealloc_ctx() again, rather use talloc_free_children(ctx). + * + * For example, to defer deallocation to the next osmo_select_main_ctx() iteration, set this to OTC_SELECT. + * + * Deferring deallocation is the simplest solution to avoid most use-after-free problems from FSM instance deallocation. + * This is a simpler and more general solution than osmo_fsm_term_safely(). + * + * To disable the feature again, pass NULL as ctx. + * + * Both osmo_fsm_term_safely() and osmo_fsm_set_dealloc_ctx() can be enabled at the same time, which will result in + * first collecting deallocated FSM instances in fsm_term_safely.collect_ctx, and finally reparenting that to the ctx + * passed here. However, in practice, it does not really make sense to enable both at the same time. + * + * \param ctx[in]  Instead of talloc_free()int, talloc_steal() all future deallocated osmo_fsm_inst instances to this + *                 ctx. If NULL, go back to talloc_free() as usual. + */ +void osmo_fsm_set_dealloc_ctx(void *ctx) +{ +	fsm_term_safely.fsm_dealloc_ctx = ctx; +} +  /*! talloc_free() the given object immediately, or once ongoing FSM terminations are done.   *   * If an FSM deallocation cascade is ongoing, talloc_steal() the given talloc_object into the talloc context that is @@ -185,7 +223,7 @@ void osmo_fsm_term_safely(bool term_safely)  static void osmo_fsm_defer_free(void *talloc_object)  {  	if (!fsm_term_safely.depth) { -		talloc_free(talloc_object); +		fsm_free_or_steal(talloc_object);  		return;  	} @@ -412,7 +450,7 @@ struct osmo_fsm_inst *osmo_fsm_inst_alloc(struct osmo_fsm *fsm, void *ctx, void  	osmo_timer_setup(&fi->timer, fsm_tmr_cb, fi);  	if (osmo_fsm_inst_update_id(fi, id) < 0) { -			talloc_free(fi); +			fsm_free_or_steal(fi);  			return NULL;  	} @@ -529,11 +567,11 @@ void osmo_fsm_inst_free(struct osmo_fsm_inst *fi)  		 * deallocate separately to avoid use-after-free errors, put it in there and deallocate all at once. */  		LOGPFSM(fi, "Deallocated, including all deferred deallocations\n");  		osmo_fsm_defer_free(fi); -		talloc_free(fsm_term_safely.collect_ctx); +		fsm_free_or_steal(fsm_term_safely.collect_ctx);  		fsm_term_safely.collect_ctx = NULL;  	} else {  		LOGPFSM(fi, "Deallocated\n"); -		talloc_free(fi); +		fsm_free_or_steal(fi);  	}  	fsm_term_safely.root_fi = NULL;  } diff --git a/tests/fsm/fsm_dealloc_test.c b/tests/fsm/fsm_dealloc_test.c index ce492056..9a6974d9 100644 --- a/tests/fsm/fsm_dealloc_test.c +++ b/tests/fsm/fsm_dealloc_test.c @@ -355,7 +355,7 @@ void obj_term(struct obj *obj)  	osmo_fsm_inst_term(obj->fi, OSMO_FSM_TERM_REGULAR, NULL);  } -void test_dealloc(enum objname trigger, bool by_destroy_event) +void test_dealloc(enum objname trigger, bool by_destroy_event, void *loop_ctx)  {  	struct scene *s = scene_alloc();  	const char *label = by_destroy_event ? "destroy-event" : "term"; @@ -383,16 +383,63 @@ void test_dealloc(enum objname trigger, bool by_destroy_event)  		LOGP(DLGLOBAL, LOGL_DEBUG, "--- %d objects remain. cleaning up\n", remain);  	} else  		LOGP(DLGLOBAL, LOGL_DEBUG, "--- all deallocated.\n"); + +	if (loop_ctx) { +		fprintf(stderr, "*** loop_ctx contains %zu blocks, deallocating.\n", +			talloc_total_blocks(loop_ctx)); +		talloc_free_children(loop_ctx); +	} + +	/* Silently free the remaining objects. */  	scene_clean(s); +	if (loop_ctx) +		talloc_free_children(loop_ctx);  } -int main(void) +static void trigger_tests(void *loop_ctx)  { -	enum objname trigger;  	size_t ctx_blocks;  	size_t ctx_size; +	enum objname trigger;  	int by_destroy_event; +	ctx_blocks = talloc_total_blocks(ctx); +	ctx_size = talloc_total_size(ctx); + +	for (trigger = 0; trigger < scene_size; trigger++) { +		for (by_destroy_event = 0; by_destroy_event < 2; by_destroy_event++) { +			test_dealloc(trigger, (bool)by_destroy_event, loop_ctx); + +			if (ctx_blocks != talloc_total_blocks(ctx) +			    || ctx_size != talloc_total_size(ctx)) { +				talloc_report_full(ctx, stderr); +				OSMO_ASSERT(false); +			} +		} +	} +} + +void test_osmo_fsm_term_safely() +{ +	fprintf(stderr, "\n\n%s()\n", __func__); +	osmo_fsm_term_safely(true); +	trigger_tests(NULL); +	osmo_fsm_term_safely(false); +	fprintf(stderr, "\n\n%s() done\n", __func__); +} + +void test_osmo_fsm_set_dealloc_ctx() +{ +	fprintf(stderr, "\n\n%s()\n", __func__); +	void *dealloc_ctx = talloc_named_const(ctx, 0, "fsm_dealloc"); +	osmo_fsm_set_dealloc_ctx(dealloc_ctx); +	trigger_tests(dealloc_ctx); +	osmo_fsm_set_dealloc_ctx(NULL); +	fprintf(stderr, "\n\n%s() done\n", __func__); +} + +int main(void) +{  	ctx = talloc_named_const(NULL, 0, "main");  	osmo_init_logging2(ctx, NULL); @@ -405,22 +452,10 @@ int main(void)  	log_set_category_filter(osmo_stderr_target, DLGLOBAL, 1, LOGL_DEBUG); -	osmo_fsm_term_safely(true);  	osmo_fsm_register(&test_fsm); -	ctx_blocks = talloc_total_blocks(ctx); -	ctx_size = talloc_total_size(ctx); - -	for (trigger = 0; trigger < scene_size; trigger++) { -		for (by_destroy_event = 0; by_destroy_event < 2; by_destroy_event++) { -			test_dealloc(trigger, (bool)by_destroy_event); -			if (ctx_blocks != talloc_total_blocks(ctx) -			    || ctx_size != talloc_total_size(ctx)) { -				talloc_report_full(ctx, stderr); -				OSMO_ASSERT(false); -			} -		} -	} +	test_osmo_fsm_term_safely(); +	test_osmo_fsm_set_dealloc_ctx();  	talloc_free(ctx);  	return 0; diff --git a/tests/fsm/fsm_dealloc_test.err b/tests/fsm/fsm_dealloc_test.err index d12c5aa3..973f3d42 100644 --- a/tests/fsm/fsm_dealloc_test.err +++ b/tests/fsm/fsm_dealloc_test.err @@ -1,3 +1,6 @@ + + +test_osmo_fsm_term_safely()  DLGLOBAL DEBUG scene_alloc()  DLGLOBAL DEBUG test(root){alive}: Allocated  DLGLOBAL DEBUG test(root){alive}: Allocated @@ -3250,3 +3253,3408 @@ DLGLOBAL DEBUG test(other){alive}: Deallocated, including all deferred deallocat  DLGLOBAL DEBUG 0 (-)  DLGLOBAL DEBUG --- after destroy-event cascade:  DLGLOBAL DEBUG --- all deallocated. + + +test_osmo_fsm_term_safely() done + + +test_osmo_fsm_set_dealloc_ctx() +DLGLOBAL DEBUG scene_alloc() +DLGLOBAL DEBUG test(root){alive}: Allocated +DLGLOBAL DEBUG test(root){alive}: Allocated +DLGLOBAL DEBUG test(root){alive}: is child of test(root) +DLGLOBAL DEBUG test(_branch0){alive}: Allocated +DLGLOBAL DEBUG test(_branch0){alive}: is child of test(_branch0) +DLGLOBAL DEBUG test(_branch0){alive}: Allocated +DLGLOBAL DEBUG test(_branch0){alive}: is child of test(_branch0) +DLGLOBAL DEBUG test(root){alive}: Allocated +DLGLOBAL DEBUG test(root){alive}: is child of test(root) +DLGLOBAL DEBUG test(_branch1){alive}: Allocated +DLGLOBAL DEBUG test(_branch1){alive}: is child of test(_branch1) +DLGLOBAL DEBUG test(_branch1){alive}: Allocated +DLGLOBAL DEBUG test(_branch1){alive}: is child of test(_branch1) +DLGLOBAL DEBUG test(other){alive}: Allocated +DLGLOBAL DEBUG test(_branch0){alive}: _branch0.other[0] = other +DLGLOBAL DEBUG test(other){alive}: other.other[0] = _branch0 +DLGLOBAL DEBUG test(__twig0a){alive}: __twig0a.other[0] = other +DLGLOBAL DEBUG test(other){alive}: other.other[1] = __twig0a +DLGLOBAL DEBUG test(_branch1){alive}: _branch1.other[0] = other +DLGLOBAL DEBUG test(other){alive}: other.other[1] = _branch1 +DLGLOBAL DEBUG test(__twig1a){alive}: __twig1a.other[0] = root +DLGLOBAL DEBUG test(root){alive}: root.other[0] = __twig1a +DLGLOBAL DEBUG ------ before term cascade, got: +DLGLOBAL DEBUG   root +DLGLOBAL DEBUG   _branch0 +DLGLOBAL DEBUG   __twig0a +DLGLOBAL DEBUG   __twig0b +DLGLOBAL DEBUG   _branch1 +DLGLOBAL DEBUG   __twig1a +DLGLOBAL DEBUG   __twig1b +DLGLOBAL DEBUG   other +DLGLOBAL DEBUG --- +DLGLOBAL DEBUG --- term at root +DLGLOBAL DEBUG test(root){alive}: Terminating (cause = OSMO_FSM_TERM_REGULAR) +DLGLOBAL DEBUG test(root){alive}: pre_term() +DLGLOBAL DEBUG test(_branch1){alive}: Terminating (cause = OSMO_FSM_TERM_PARENT) +DLGLOBAL DEBUG test(_branch1){alive}: pre_term() +DLGLOBAL DEBUG test(__twig1b){alive}: Terminating (cause = OSMO_FSM_TERM_PARENT) +DLGLOBAL DEBUG test(__twig1b){alive}: pre_term() +DLGLOBAL DEBUG test(__twig1b){alive}: Removing from parent test(_branch1) +DLGLOBAL DEBUG 1 (__twig1b.cleanup()) +DLGLOBAL DEBUG test(__twig1b){alive}: cleanup() +DLGLOBAL DEBUG test(__twig1b){alive}: scene forgets __twig1b +DLGLOBAL DEBUG test(_branch1){alive}: Received Event EV_CHILD_GONE +DLGLOBAL DEBUG 2 (__twig1b.cleanup(),_branch1.alive()) +DLGLOBAL DEBUG test(_branch1){alive}: alive(EV_CHILD_GONE) +DLGLOBAL DEBUG 3 (__twig1b.cleanup(),_branch1.alive(),_branch1.child_gone()) +DLGLOBAL DEBUG test(_branch1){alive}: EV_CHILD_GONE: Dropped reference _branch1.child[1] = __twig1b +DLGLOBAL DEBUG test(_branch1){alive}: still exists: child[0] +DLGLOBAL DEBUG 2 (__twig1b.cleanup(),_branch1.alive()) +DLGLOBAL DEBUG 1 (__twig1b.cleanup()) +DLGLOBAL DEBUG test(__twig1b){alive}: cleanup() done +DLGLOBAL DEBUG 0 (-) +DLGLOBAL DEBUG test(__twig1b){alive}: Freeing instance +DLGLOBAL DEBUG test(__twig1b){alive}: Deallocated +DLGLOBAL DEBUG test(__twig1a){alive}: Terminating (cause = OSMO_FSM_TERM_PARENT) +DLGLOBAL DEBUG test(__twig1a){alive}: pre_term() +DLGLOBAL DEBUG test(__twig1a){alive}: Removing from parent test(_branch1) +DLGLOBAL DEBUG 1 (__twig1a.cleanup()) +DLGLOBAL DEBUG test(__twig1a){alive}: cleanup() +DLGLOBAL DEBUG test(__twig1a){alive}: scene forgets __twig1a +DLGLOBAL DEBUG test(__twig1a){alive}: removing reference __twig1a.other[0] -> root +DLGLOBAL DEBUG test(root){alive}: Received Event EV_OTHER_GONE +DLGLOBAL DEBUG 2 (__twig1a.cleanup(),root.alive()) +DLGLOBAL DEBUG test(root){alive}: alive(EV_OTHER_GONE) +DLGLOBAL DEBUG 3 (__twig1a.cleanup(),root.alive(),root.other_gone()) +DLGLOBAL DEBUG test(root){alive}: EV_OTHER_GONE: Dropped reference root.other[0] = __twig1a +DLGLOBAL DEBUG 2 (__twig1a.cleanup(),root.alive()) +DLGLOBAL DEBUG test(root){alive}: Ignoring trigger to terminate: already terminating +DLGLOBAL DEBUG 1 (__twig1a.cleanup()) +DLGLOBAL DEBUG test(_branch1){alive}: Received Event EV_CHILD_GONE +DLGLOBAL DEBUG 2 (__twig1a.cleanup(),_branch1.alive()) +DLGLOBAL DEBUG test(_branch1){alive}: alive(EV_CHILD_GONE) +DLGLOBAL DEBUG 3 (__twig1a.cleanup(),_branch1.alive(),_branch1.child_gone()) +DLGLOBAL DEBUG test(_branch1){alive}: EV_CHILD_GONE: Dropped reference _branch1.child[0] = __twig1a +DLGLOBAL DEBUG test(_branch1){alive}: No more children +DLGLOBAL DEBUG 2 (__twig1a.cleanup(),_branch1.alive()) +DLGLOBAL DEBUG test(_branch1){alive}: Ignoring trigger to terminate: already terminating +DLGLOBAL DEBUG 1 (__twig1a.cleanup()) +DLGLOBAL DEBUG test(__twig1a){alive}: cleanup() done +DLGLOBAL DEBUG 0 (-) +DLGLOBAL DEBUG test(__twig1a){alive}: Freeing instance +DLGLOBAL DEBUG test(__twig1a){alive}: Deallocated +DLGLOBAL DEBUG test(_branch1){alive}: Removing from parent test(root) +DLGLOBAL DEBUG 1 (_branch1.cleanup()) +DLGLOBAL DEBUG test(_branch1){alive}: cleanup() +DLGLOBAL DEBUG test(_branch1){alive}: scene forgets _branch1 +DLGLOBAL DEBUG test(_branch1){alive}: removing reference _branch1.other[0] -> other +DLGLOBAL DEBUG test(other){alive}: Received Event EV_OTHER_GONE +DLGLOBAL DEBUG 2 (_branch1.cleanup(),other.alive()) +DLGLOBAL DEBUG test(other){alive}: alive(EV_OTHER_GONE) +DLGLOBAL DEBUG 3 (_branch1.cleanup(),other.alive(),other.other_gone()) +DLGLOBAL DEBUG test(other){alive}: EV_OTHER_GONE: Dropped reference other.other[1] = _branch1 +DLGLOBAL DEBUG 2 (_branch1.cleanup(),other.alive()) +DLGLOBAL DEBUG test(other){alive}: Terminating (cause = OSMO_FSM_TERM_REGULAR) +DLGLOBAL DEBUG test(other){alive}: pre_term() +DLGLOBAL DEBUG 3 (_branch1.cleanup(),other.alive(),other.cleanup()) +DLGLOBAL DEBUG test(other){alive}: cleanup() +DLGLOBAL DEBUG test(other){alive}: scene forgets other +DLGLOBAL DEBUG test(other){alive}: removing reference other.other[0] -> _branch0 +DLGLOBAL DEBUG test(_branch0){alive}: Received Event EV_OTHER_GONE +DLGLOBAL DEBUG 4 (_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive()) +DLGLOBAL DEBUG test(_branch0){alive}: alive(EV_OTHER_GONE) +DLGLOBAL DEBUG 5 (_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive(),_branch0.other_gone()) +DLGLOBAL DEBUG test(_branch0){alive}: EV_OTHER_GONE: Dropped reference _branch0.other[0] = other +DLGLOBAL DEBUG 4 (_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive()) +DLGLOBAL DEBUG test(_branch0){alive}: Terminating (cause = OSMO_FSM_TERM_REGULAR) +DLGLOBAL DEBUG test(_branch0){alive}: pre_term() +DLGLOBAL DEBUG test(__twig0b){alive}: Terminating (cause = OSMO_FSM_TERM_PARENT) +DLGLOBAL DEBUG test(__twig0b){alive}: pre_term() +DLGLOBAL DEBUG test(__twig0b){alive}: Removing from parent test(_branch0) +DLGLOBAL DEBUG 5 (_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive(),__twig0b.cleanup()) +DLGLOBAL DEBUG test(__twig0b){alive}: cleanup() +DLGLOBAL DEBUG test(__twig0b){alive}: scene forgets __twig0b +DLGLOBAL DEBUG test(_branch0){alive}: Received Event EV_CHILD_GONE +DLGLOBAL DEBUG 6 (_branch1.cleanup(),other.alive(),other.cleanup(),2*_branch0.alive(),__twig0b.cleanup()) +DLGLOBAL DEBUG test(_branch0){alive}: alive(EV_CHILD_GONE) +DLGLOBAL DEBUG 7 (_branch1.cleanup(),other.alive(),other.cleanup(),2*_branch0.alive(),__twig0b.cleanup(),_branch0.child_gone()) +DLGLOBAL DEBUG test(_branch0){alive}: EV_CHILD_GONE: Dropped reference _branch0.child[1] = __twig0b +DLGLOBAL DEBUG test(_branch0){alive}: still exists: child[0] +DLGLOBAL DEBUG 6 (_branch1.cleanup(),other.alive(),other.cleanup(),2*_branch0.alive(),__twig0b.cleanup()) +DLGLOBAL DEBUG 5 (_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive(),__twig0b.cleanup()) +DLGLOBAL DEBUG test(__twig0b){alive}: cleanup() done +DLGLOBAL DEBUG 4 (_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive()) +DLGLOBAL DEBUG test(__twig0b){alive}: Freeing instance +DLGLOBAL DEBUG test(__twig0b){alive}: Deallocated +DLGLOBAL DEBUG test(__twig0a){alive}: Terminating (cause = OSMO_FSM_TERM_PARENT) +DLGLOBAL DEBUG test(__twig0a){alive}: pre_term() +DLGLOBAL DEBUG test(__twig0a){alive}: Removing from parent test(_branch0) +DLGLOBAL DEBUG 5 (_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive(),__twig0a.cleanup()) +DLGLOBAL DEBUG test(__twig0a){alive}: cleanup() +DLGLOBAL DEBUG test(__twig0a){alive}: scene forgets __twig0a +DLGLOBAL DEBUG test(__twig0a){alive}: removing reference __twig0a.other[0] -> other +DLGLOBAL DEBUG test(other){alive}: Received Event EV_OTHER_GONE +DLGLOBAL DEBUG 6 (_branch1.cleanup(),2*other.alive(),other.cleanup(),_branch0.alive(),__twig0a.cleanup()) +DLGLOBAL DEBUG test(other){alive}: alive(EV_OTHER_GONE) +DLGLOBAL DEBUG 7 (_branch1.cleanup(),2*other.alive(),other.cleanup(),_branch0.alive(),__twig0a.cleanup(),other.other_gone()) +DLGLOBAL DEBUG 6 (_branch1.cleanup(),2*other.alive(),other.cleanup(),_branch0.alive(),__twig0a.cleanup()) +DLGLOBAL DEBUG 5 (_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive(),__twig0a.cleanup()) +DLGLOBAL DEBUG test(_branch0){alive}: Received Event EV_CHILD_GONE +DLGLOBAL DEBUG 6 (_branch1.cleanup(),other.alive(),other.cleanup(),2*_branch0.alive(),__twig0a.cleanup()) +DLGLOBAL DEBUG test(_branch0){alive}: alive(EV_CHILD_GONE) +DLGLOBAL DEBUG 7 (_branch1.cleanup(),other.alive(),other.cleanup(),2*_branch0.alive(),__twig0a.cleanup(),_branch0.child_gone()) +DLGLOBAL DEBUG test(_branch0){alive}: EV_CHILD_GONE: Dropped reference _branch0.child[0] = __twig0a +DLGLOBAL DEBUG test(_branch0){alive}: No more children +DLGLOBAL DEBUG 6 (_branch1.cleanup(),other.alive(),other.cleanup(),2*_branch0.alive(),__twig0a.cleanup()) +DLGLOBAL DEBUG test(_branch0){alive}: Ignoring trigger to terminate: already terminating +DLGLOBAL DEBUG 5 (_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive(),__twig0a.cleanup()) +DLGLOBAL DEBUG test(__twig0a){alive}: cleanup() done +DLGLOBAL DEBUG 4 (_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive()) +DLGLOBAL DEBUG test(__twig0a){alive}: Freeing instance +DLGLOBAL DEBUG test(__twig0a){alive}: Deallocated +DLGLOBAL DEBUG test(_branch0){alive}: Removing from parent test(root) +DLGLOBAL DEBUG 5 (_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive(),_branch0.cleanup()) +DLGLOBAL DEBUG test(_branch0){alive}: cleanup() +DLGLOBAL DEBUG test(_branch0){alive}: scene forgets _branch0 +DLGLOBAL DEBUG test(root){alive}: Received Event EV_CHILD_GONE +DLGLOBAL DEBUG 6 (_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive(),_branch0.cleanup(),root.alive()) +DLGLOBAL DEBUG test(root){alive}: alive(EV_CHILD_GONE) +DLGLOBAL DEBUG 7 (_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive(),_branch0.cleanup(),root.alive(),root.child_gone()) +DLGLOBAL DEBUG test(root){alive}: EV_CHILD_GONE: Dropped reference root.child[0] = _branch0 +DLGLOBAL DEBUG test(root){alive}: still exists: child[1] +DLGLOBAL DEBUG 6 (_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive(),_branch0.cleanup(),root.alive()) +DLGLOBAL DEBUG 5 (_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive(),_branch0.cleanup()) +DLGLOBAL DEBUG test(_branch0){alive}: cleanup() done +DLGLOBAL DEBUG 4 (_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive()) +DLGLOBAL DEBUG test(_branch0){alive}: Freeing instance +DLGLOBAL DEBUG test(_branch0){alive}: Deallocated +DLGLOBAL DEBUG test(root){alive}: Received Event EV_CHILD_GONE +DLGLOBAL DEBUG 5 (_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive(),root.alive()) +DLGLOBAL DEBUG test(root){alive}: alive(EV_CHILD_GONE) +DLGLOBAL DEBUG test(root){alive}: EV_CHILD_GONE with NULL data, must be a parent_term event. Ignore. +DLGLOBAL DEBUG 4 (_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive()) +DLGLOBAL DEBUG 3 (_branch1.cleanup(),other.alive(),other.cleanup()) +DLGLOBAL DEBUG test(other){alive}: cleanup() done +DLGLOBAL DEBUG 2 (_branch1.cleanup(),other.alive()) +DLGLOBAL DEBUG test(other){alive}: Freeing instance +DLGLOBAL DEBUG test(other){alive}: Deallocated +DLGLOBAL DEBUG 1 (_branch1.cleanup()) +DLGLOBAL DEBUG test(root){alive}: Received Event EV_CHILD_GONE +DLGLOBAL DEBUG 2 (_branch1.cleanup(),root.alive()) +DLGLOBAL DEBUG test(root){alive}: alive(EV_CHILD_GONE) +DLGLOBAL DEBUG 3 (_branch1.cleanup(),root.alive(),root.child_gone()) +DLGLOBAL DEBUG test(root){alive}: EV_CHILD_GONE: Dropped reference root.child[1] = _branch1 +DLGLOBAL DEBUG test(root){alive}: No more children +DLGLOBAL DEBUG 2 (_branch1.cleanup(),root.alive()) +DLGLOBAL DEBUG test(root){alive}: Ignoring trigger to terminate: already terminating +DLGLOBAL DEBUG 1 (_branch1.cleanup()) +DLGLOBAL DEBUG test(_branch1){alive}: cleanup() done +DLGLOBAL DEBUG 0 (-) +DLGLOBAL DEBUG test(_branch1){alive}: Freeing instance +DLGLOBAL DEBUG test(_branch1){alive}: Deallocated +DLGLOBAL DEBUG 1 (root.cleanup()) +DLGLOBAL DEBUG test(root){alive}: cleanup() +DLGLOBAL DEBUG test(root){alive}: scene forgets root +DLGLOBAL DEBUG test(root){alive}: cleanup() done +DLGLOBAL DEBUG 0 (-) +DLGLOBAL DEBUG test(root){alive}: Freeing instance +DLGLOBAL DEBUG test(root){alive}: Deallocated +DLGLOBAL DEBUG --- after term cascade: +DLGLOBAL DEBUG --- all deallocated. +*** loop_ctx contains 33 blocks, deallocating. +DLGLOBAL DEBUG scene_alloc() +DLGLOBAL DEBUG test(root){alive}: Allocated +DLGLOBAL DEBUG test(root){alive}: Allocated +DLGLOBAL DEBUG test(root){alive}: is child of test(root) +DLGLOBAL DEBUG test(_branch0){alive}: Allocated +DLGLOBAL DEBUG test(_branch0){alive}: is child of test(_branch0) +DLGLOBAL DEBUG test(_branch0){alive}: Allocated +DLGLOBAL DEBUG test(_branch0){alive}: is child of test(_branch0) +DLGLOBAL DEBUG test(root){alive}: Allocated +DLGLOBAL DEBUG test(root){alive}: is child of test(root) +DLGLOBAL DEBUG test(_branch1){alive}: Allocated +DLGLOBAL DEBUG test(_branch1){alive}: is child of test(_branch1) +DLGLOBAL DEBUG test(_branch1){alive}: Allocated +DLGLOBAL DEBUG test(_branch1){alive}: is child of test(_branch1) +DLGLOBAL DEBUG test(other){alive}: Allocated +DLGLOBAL DEBUG test(_branch0){alive}: _branch0.other[0] = other +DLGLOBAL DEBUG test(other){alive}: other.other[0] = _branch0 +DLGLOBAL DEBUG test(__twig0a){alive}: __twig0a.other[0] = other +DLGLOBAL DEBUG test(other){alive}: other.other[1] = __twig0a +DLGLOBAL DEBUG test(_branch1){alive}: _branch1.other[0] = other +DLGLOBAL DEBUG test(other){alive}: other.other[1] = _branch1 +DLGLOBAL DEBUG test(__twig1a){alive}: __twig1a.other[0] = root +DLGLOBAL DEBUG test(root){alive}: root.other[0] = __twig1a +DLGLOBAL DEBUG ------ before destroy-event cascade, got: +DLGLOBAL DEBUG   root +DLGLOBAL DEBUG   _branch0 +DLGLOBAL DEBUG   __twig0a +DLGLOBAL DEBUG   __twig0b +DLGLOBAL DEBUG   _branch1 +DLGLOBAL DEBUG   __twig1a +DLGLOBAL DEBUG   __twig1b +DLGLOBAL DEBUG   other +DLGLOBAL DEBUG --- +DLGLOBAL DEBUG --- destroy-event at root +DLGLOBAL DEBUG test(root){alive}: Received Event EV_DESTROY +DLGLOBAL DEBUG 1 (root.alive()) +DLGLOBAL DEBUG test(root){alive}: alive(EV_DESTROY) +DLGLOBAL DEBUG test(root){alive}: Terminating (cause = OSMO_FSM_TERM_REGULAR) +DLGLOBAL DEBUG test(root){alive}: pre_term() +DLGLOBAL DEBUG test(_branch1){alive}: Terminating (cause = OSMO_FSM_TERM_PARENT) +DLGLOBAL DEBUG test(_branch1){alive}: pre_term() +DLGLOBAL DEBUG test(__twig1b){alive}: Terminating (cause = OSMO_FSM_TERM_PARENT) +DLGLOBAL DEBUG test(__twig1b){alive}: pre_term() +DLGLOBAL DEBUG test(__twig1b){alive}: Removing from parent test(_branch1) +DLGLOBAL DEBUG 2 (root.alive(),__twig1b.cleanup()) +DLGLOBAL DEBUG test(__twig1b){alive}: cleanup() +DLGLOBAL DEBUG test(__twig1b){alive}: scene forgets __twig1b +DLGLOBAL DEBUG test(_branch1){alive}: Received Event EV_CHILD_GONE +DLGLOBAL DEBUG 3 (root.alive(),__twig1b.cleanup(),_branch1.alive()) +DLGLOBAL DEBUG test(_branch1){alive}: alive(EV_CHILD_GONE) +DLGLOBAL DEBUG 4 (root.alive(),__twig1b.cleanup(),_branch1.alive(),_branch1.child_gone()) +DLGLOBAL DEBUG test(_branch1){alive}: EV_CHILD_GONE: Dropped reference _branch1.child[1] = __twig1b +DLGLOBAL DEBUG test(_branch1){alive}: still exists: child[0] +DLGLOBAL DEBUG 3 (root.alive(),__twig1b.cleanup(),_branch1.alive()) +DLGLOBAL DEBUG 2 (root.alive(),__twig1b.cleanup()) +DLGLOBAL DEBUG test(__twig1b){alive}: cleanup() done +DLGLOBAL DEBUG 1 (root.alive()) +DLGLOBAL DEBUG test(__twig1b){alive}: Freeing instance +DLGLOBAL DEBUG test(__twig1b){alive}: Deallocated +DLGLOBAL DEBUG test(__twig1a){alive}: Terminating (cause = OSMO_FSM_TERM_PARENT) +DLGLOBAL DEBUG test(__twig1a){alive}: pre_term() +DLGLOBAL DEBUG test(__twig1a){alive}: Removing from parent test(_branch1) +DLGLOBAL DEBUG 2 (root.alive(),__twig1a.cleanup()) +DLGLOBAL DEBUG test(__twig1a){alive}: cleanup() +DLGLOBAL DEBUG test(__twig1a){alive}: scene forgets __twig1a +DLGLOBAL DEBUG test(__twig1a){alive}: removing reference __twig1a.other[0] -> root +DLGLOBAL DEBUG test(root){alive}: Received Event EV_OTHER_GONE +DLGLOBAL DEBUG 3 (2*root.alive(),__twig1a.cleanup()) +DLGLOBAL DEBUG test(root){alive}: alive(EV_OTHER_GONE) +DLGLOBAL DEBUG 4 (2*root.alive(),__twig1a.cleanup(),root.other_gone()) +DLGLOBAL DEBUG test(root){alive}: EV_OTHER_GONE: Dropped reference root.other[0] = __twig1a +DLGLOBAL DEBUG 3 (2*root.alive(),__twig1a.cleanup()) +DLGLOBAL DEBUG test(root){alive}: Ignoring trigger to terminate: already terminating +DLGLOBAL DEBUG 2 (root.alive(),__twig1a.cleanup()) +DLGLOBAL DEBUG test(_branch1){alive}: Received Event EV_CHILD_GONE +DLGLOBAL DEBUG 3 (root.alive(),__twig1a.cleanup(),_branch1.alive()) +DLGLOBAL DEBUG test(_branch1){alive}: alive(EV_CHILD_GONE) +DLGLOBAL DEBUG 4 (root.alive(),__twig1a.cleanup(),_branch1.alive(),_branch1.child_gone()) +DLGLOBAL DEBUG test(_branch1){alive}: EV_CHILD_GONE: Dropped reference _branch1.child[0] = __twig1a +DLGLOBAL DEBUG test(_branch1){alive}: No more children +DLGLOBAL DEBUG 3 (root.alive(),__twig1a.cleanup(),_branch1.alive()) +DLGLOBAL DEBUG test(_branch1){alive}: Ignoring trigger to terminate: already terminating +DLGLOBAL DEBUG 2 (root.alive(),__twig1a.cleanup()) +DLGLOBAL DEBUG test(__twig1a){alive}: cleanup() done +DLGLOBAL DEBUG 1 (root.alive()) +DLGLOBAL DEBUG test(__twig1a){alive}: Freeing instance +DLGLOBAL DEBUG test(__twig1a){alive}: Deallocated +DLGLOBAL DEBUG test(_branch1){alive}: Removing from parent test(root) +DLGLOBAL DEBUG 2 (root.alive(),_branch1.cleanup()) +DLGLOBAL DEBUG test(_branch1){alive}: cleanup() +DLGLOBAL DEBUG test(_branch1){alive}: scene forgets _branch1 +DLGLOBAL DEBUG test(_branch1){alive}: removing reference _branch1.other[0] -> other +DLGLOBAL DEBUG test(other){alive}: Received Event EV_OTHER_GONE +DLGLOBAL DEBUG 3 (root.alive(),_branch1.cleanup(),other.alive()) +DLGLOBAL DEBUG test(other){alive}: alive(EV_OTHER_GONE) +DLGLOBAL DEBUG 4 (root.alive(),_branch1.cleanup(),other.alive(),other.other_gone()) +DLGLOBAL DEBUG test(other){alive}: EV_OTHER_GONE: Dropped reference other.other[1] = _branch1 +DLGLOBAL DEBUG 3 (root.alive(),_branch1.cleanup(),other.alive()) +DLGLOBAL DEBUG test(other){alive}: Terminating (cause = OSMO_FSM_TERM_REGULAR) +DLGLOBAL DEBUG test(other){alive}: pre_term() +DLGLOBAL DEBUG 4 (root.alive(),_branch1.cleanup(),other.alive(),other.cleanup()) +DLGLOBAL DEBUG test(other){alive}: cleanup() +DLGLOBAL DEBUG test(other){alive}: scene forgets other +DLGLOBAL DEBUG test(other){alive}: removing reference other.other[0] -> _branch0 +DLGLOBAL DEBUG test(_branch0){alive}: Received Event EV_OTHER_GONE +DLGLOBAL DEBUG 5 (root.alive(),_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive()) +DLGLOBAL DEBUG test(_branch0){alive}: alive(EV_OTHER_GONE) +DLGLOBAL DEBUG 6 (root.alive(),_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive(),_branch0.other_gone()) +DLGLOBAL DEBUG test(_branch0){alive}: EV_OTHER_GONE: Dropped reference _branch0.other[0] = other +DLGLOBAL DEBUG 5 (root.alive(),_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive()) +DLGLOBAL DEBUG test(_branch0){alive}: Terminating (cause = OSMO_FSM_TERM_REGULAR) +DLGLOBAL DEBUG test(_branch0){alive}: pre_term() +DLGLOBAL DEBUG test(__twig0b){alive}: Terminating (cause = OSMO_FSM_TERM_PARENT) +DLGLOBAL DEBUG test(__twig0b){alive}: pre_term() +DLGLOBAL DEBUG test(__twig0b){alive}: Removing from parent test(_branch0) +DLGLOBAL DEBUG 6 (root.alive(),_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive(),__twig0b.cleanup()) +DLGLOBAL DEBUG test(__twig0b){alive}: cleanup() +DLGLOBAL DEBUG test(__twig0b){alive}: scene forgets __twig0b +DLGLOBAL DEBUG test(_branch0){alive}: Received Event EV_CHILD_GONE +DLGLOBAL DEBUG 7 (root.alive(),_branch1.cleanup(),other.alive(),other.cleanup(),2*_branch0.alive(),__twig0b.cleanup()) +DLGLOBAL DEBUG test(_branch0){alive}: alive(EV_CHILD_GONE) +DLGLOBAL DEBUG 8 (root.alive(),_branch1.cleanup(),other.alive(),other.cleanup(),2*_branch0.alive(),__twig0b.cleanup(),_branch0.child_gone()) +DLGLOBAL DEBUG test(_branch0){alive}: EV_CHILD_GONE: Dropped reference _branch0.child[1] = __twig0b +DLGLOBAL DEBUG test(_branch0){alive}: still exists: child[0] +DLGLOBAL DEBUG 7 (root.alive(),_branch1.cleanup(),other.alive(),other.cleanup(),2*_branch0.alive(),__twig0b.cleanup()) +DLGLOBAL DEBUG 6 (root.alive(),_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive(),__twig0b.cleanup()) +DLGLOBAL DEBUG test(__twig0b){alive}: cleanup() done +DLGLOBAL DEBUG 5 (root.alive(),_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive()) +DLGLOBAL DEBUG test(__twig0b){alive}: Freeing instance +DLGLOBAL DEBUG test(__twig0b){alive}: Deallocated +DLGLOBAL DEBUG test(__twig0a){alive}: Terminating (cause = OSMO_FSM_TERM_PARENT) +DLGLOBAL DEBUG test(__twig0a){alive}: pre_term() +DLGLOBAL DEBUG test(__twig0a){alive}: Removing from parent test(_branch0) +DLGLOBAL DEBUG 6 (root.alive(),_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive(),__twig0a.cleanup()) +DLGLOBAL DEBUG test(__twig0a){alive}: cleanup() +DLGLOBAL DEBUG test(__twig0a){alive}: scene forgets __twig0a +DLGLOBAL DEBUG test(__twig0a){alive}: removing reference __twig0a.other[0] -> other +DLGLOBAL DEBUG test(other){alive}: Received Event EV_OTHER_GONE +DLGLOBAL DEBUG 7 (root.alive(),_branch1.cleanup(),2*other.alive(),other.cleanup(),_branch0.alive(),__twig0a.cleanup()) +DLGLOBAL DEBUG test(other){alive}: alive(EV_OTHER_GONE) +DLGLOBAL DEBUG 8 (root.alive(),_branch1.cleanup(),2*other.alive(),other.cleanup(),_branch0.alive(),__twig0a.cleanup(),other.other_gone()) +DLGLOBAL DEBUG 7 (root.alive(),_branch1.cleanup(),2*other.alive(),other.cleanup(),_branch0.alive(),__twig0a.cleanup()) +DLGLOBAL DEBUG 6 (root.alive(),_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive(),__twig0a.cleanup()) +DLGLOBAL DEBUG test(_branch0){alive}: Received Event EV_CHILD_GONE +DLGLOBAL DEBUG 7 (root.alive(),_branch1.cleanup(),other.alive(),other.cleanup(),2*_branch0.alive(),__twig0a.cleanup()) +DLGLOBAL DEBUG test(_branch0){alive}: alive(EV_CHILD_GONE) +DLGLOBAL DEBUG 8 (root.alive(),_branch1.cleanup(),other.alive(),other.cleanup(),2*_branch0.alive(),__twig0a.cleanup(),_branch0.child_gone()) +DLGLOBAL DEBUG test(_branch0){alive}: EV_CHILD_GONE: Dropped reference _branch0.child[0] = __twig0a +DLGLOBAL DEBUG test(_branch0){alive}: No more children +DLGLOBAL DEBUG 7 (root.alive(),_branch1.cleanup(),other.alive(),other.cleanup(),2*_branch0.alive(),__twig0a.cleanup()) +DLGLOBAL DEBUG test(_branch0){alive}: Ignoring trigger to terminate: already terminating +DLGLOBAL DEBUG 6 (root.alive(),_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive(),__twig0a.cleanup()) +DLGLOBAL DEBUG test(__twig0a){alive}: cleanup() done +DLGLOBAL DEBUG 5 (root.alive(),_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive()) +DLGLOBAL DEBUG test(__twig0a){alive}: Freeing instance +DLGLOBAL DEBUG test(__twig0a){alive}: Deallocated +DLGLOBAL DEBUG test(_branch0){alive}: Removing from parent test(root) +DLGLOBAL DEBUG 6 (root.alive(),_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive(),_branch0.cleanup()) +DLGLOBAL DEBUG test(_branch0){alive}: cleanup() +DLGLOBAL DEBUG test(_branch0){alive}: scene forgets _branch0 +DLGLOBAL DEBUG test(root){alive}: Received Event EV_CHILD_GONE +DLGLOBAL DEBUG 7 (2*root.alive(),_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive(),_branch0.cleanup()) +DLGLOBAL DEBUG test(root){alive}: alive(EV_CHILD_GONE) +DLGLOBAL DEBUG 8 (2*root.alive(),_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive(),_branch0.cleanup(),root.child_gone()) +DLGLOBAL DEBUG test(root){alive}: EV_CHILD_GONE: Dropped reference root.child[0] = _branch0 +DLGLOBAL DEBUG test(root){alive}: still exists: child[1] +DLGLOBAL DEBUG 7 (2*root.alive(),_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive(),_branch0.cleanup()) +DLGLOBAL DEBUG 6 (root.alive(),_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive(),_branch0.cleanup()) +DLGLOBAL DEBUG test(_branch0){alive}: cleanup() done +DLGLOBAL DEBUG 5 (root.alive(),_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive()) +DLGLOBAL DEBUG test(_branch0){alive}: Freeing instance +DLGLOBAL DEBUG test(_branch0){alive}: Deallocated +DLGLOBAL DEBUG test(root){alive}: Received Event EV_CHILD_GONE +DLGLOBAL DEBUG 6 (2*root.alive(),_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive()) +DLGLOBAL DEBUG test(root){alive}: alive(EV_CHILD_GONE) +DLGLOBAL DEBUG test(root){alive}: EV_CHILD_GONE with NULL data, must be a parent_term event. Ignore. +DLGLOBAL DEBUG 5 (root.alive(),_branch1.cleanup(),other.alive(),other.cleanup(),_branch0.alive()) +DLGLOBAL DEBUG 4 (root.alive(),_branch1.cleanup(),other.alive(),other.cleanup()) +DLGLOBAL DEBUG test(other){alive}: cleanup() done +DLGLOBAL DEBUG 3 (root.alive(),_branch1.cleanup(),other.alive()) +DLGLOBAL DEBUG test(other){alive}: Freeing instance +DLGLOBAL DEBUG test(other){alive}: Deallocated +DLGLOBAL DEBUG 2 (root.alive(),_branch1.cleanup()) +DLGLOBAL DEBUG test(root){alive}: Received Event EV_CHILD_GONE +DLGLOBAL DEBUG 3 (2*root.alive(),_branch1.cleanup()) +DLGLOBAL DEBUG test(root){alive}: alive(EV_CHILD_GONE) +DLGLOBAL DEBUG 4 (2*root.alive(),_branch1.cleanup(),root.child_gone()) +DLGLOBAL DEBUG test(root){alive}: EV_CHILD_GONE: Dropped reference root.child[1] = _branch1 +DLGLOBAL DEBUG test(root){alive}: No more children +DLGLOBAL DEBUG 3 (2*root.alive(),_branch1.cleanup()) +DLGLOBAL DEBUG test(root){alive}: Ignoring trigger to terminate: already terminating +DLGLOBAL DEBUG 2 (root.alive(),_branch1.cleanup()) +DLGLOBAL DEBUG test(_branch1){alive}: cleanup() done +DLGLOBAL DEBUG 1 (root.alive()) +DLGLOBAL DEBUG test(_branch1){alive}: Freeing instance +DLGLOBAL DEBUG test(_branch1){alive}: Deallocated +DLGLOBAL DEBUG 2 (root.alive(),root.cleanup()) +DLGLOBAL DEBUG test(root){alive}: cleanup() +DLGLOBAL DEBUG test(root){alive}: scene forgets root +DLGLOBAL DEBUG test(root){alive}: cleanup() done +DLGLOBAL DEBUG 1 (root.alive()) +DLGLOBAL DEBUG test(root){alive}: Freeing instance +DLGLOBAL DEBUG test(root){alive}: Deallocated +DLGLOBAL DEBUG 0 (-) +DLGLOBAL DEBUG --- after destroy-event cascade: +DLGLOBAL DEBUG --- all deallocated. +*** loop_ctx contains 33 blocks, deallocating. +DLGLOBAL DEBUG scene_alloc() +DLGLOBAL DEBUG test(root){alive}: Allocated +DLGLOBAL DEBUG test(root){alive}: Allocated +DLGLOBAL DEBUG test(root){alive}: is child of test(root) +DLGLOBAL DEBUG test(_branch0){alive}: Allocated +DLGLOBAL DEBUG test(_branch0){alive}: is child of test(_branch0) +DLGLOBAL DEBUG test(_branch0){alive}: Allocated +DLGLOBAL DEBUG test(_branch0){alive}: is child of test(_branch0) +DLGLOBAL DEBUG test(root){alive}: Allocated +DLGLOBAL DEBUG test(root){alive}: is child of test(root) +DLGLOBAL DEBUG test(_branch1){alive}: Allocated +DLGLOBAL DEBUG test(_branch1){alive}: is child of test(_branch1) +DLGLOBAL DEBUG test(_branch1){alive}: Allocated +DLGLOBAL DEBUG test(_branch1){alive}: is child of test(_branch1) +DLGLOBAL DEBUG test(other){alive}: Allocated +DLGLOBAL DEBUG test(_branch0){alive}: _branch0.other[0] = other +DLGLOBAL DEBUG test(other){alive}: other.other[0] = _branch0 +DLGLOBAL DEBUG test(__twig0a){alive}: __twig0a.other[0] = other +DLGLOBAL DEBUG test(other){alive}: other.other[1] = __twig0a +DLGLOBAL DEBUG test(_branch1){alive}: _branch1.other[0] = other +DLGLOBAL DEBUG test(other){alive}: other.other[1] = _branch1 +DLGLOBAL DEBUG test(__twig1a){alive}: __twig1a.other[0] = root +DLGLOBAL DEBUG test(root){alive}: root.other[0] = __twig1a +DLGLOBAL DEBUG ------ before term cascade, got: +DLGLOBAL DEBUG   root +DLGLOBAL DEBUG   _branch0 +DLGLOBAL DEBUG   __twig0a +DLGLOBAL DEBUG   __twig0b +DLGLOBAL DEBUG   _branch1 +DLGLOBAL DEBUG   __twig1a +DLGLOBAL DEBUG   __twig1b +DLGLOBAL DEBUG   other +DLGLOBAL DEBUG --- +DLGLOBAL DEBUG --- term at _branch0 +DLGLOBAL DEBUG test(_branch0){alive}: Terminating (cause = OSMO_FSM_TERM_REGULAR) +DLGLOBAL DEBUG test(_branch0){alive}: pre_term() +DLGLOBAL DEBUG test(__twig0b){alive}: Terminating (cause = OSMO_FSM_TERM_PARENT) +DLGLOBAL DEBUG test(__twig0b){alive}: pre_term() +DLGLOBAL DEBUG test(__twig0b){alive}: Removing from parent test(_branch0) +DLGLOBAL DEBUG 1 (__twig0b.cleanup()) +DLGLOBAL DEBUG test(__twig0b){alive}: cleanup() +DLGLOBAL DEBUG test(__twig0b){alive}: scene forgets __twig0b +DLGLOBAL DEBUG test(_branch0){alive}: Received Event EV_CHILD_GONE +DLGLOBAL DEBUG 2 (__twig0b.cleanup(),_branch0.alive()) +DLGLOBAL DEBUG test(_branch0){alive}: alive(EV_CHILD_GONE) +DLGLOBAL DEBUG 3 (__twig0b.cleanup(),_branch0.alive(),_branch0.child_gone()) +DLGLOBAL DEBUG test(_branch0){alive}: EV_CHILD_GONE: Dropped reference _branch0.child[1] = __twig0b +DLGLOBAL DEBUG test(_branch0){alive}: still exists: child[0] +DLGLOBAL DEBUG 2 (__twig0b.cleanup(),_branch0.alive()) +DLGLOBAL DEBUG 1 (__twig0b.cleanup()) +DLGLOBAL DEBUG test(__twig0b){alive}: cleanup() done +DLGLOBAL DEBUG 0 (-) +DLGLOBAL DEBUG test(__twig0b){alive}: Freeing instance +DLGLOBAL DEBUG test(__twig0b){alive}: Deallocated +DLGLOBAL DEBUG test(__twig0a){alive}: Terminating (cause = OSMO_FSM_TERM_PARENT) +DLGLOBAL DEBUG test(__twig0a){alive}: pre_term() +DLGLOBAL DEBUG test(__twig0a){alive}: Removing from parent test(_branch0) +DLGLOBAL DEBUG 1 (__twig0a.cleanup()) +DLGLOBAL DEBUG test(__twig0a){alive}: cleanup() +DLGLOBAL DEBUG test(__twig0a){alive}: scene forgets __twig0a +DLGLOBAL DEBUG test(__twig0a){alive}: removing reference __twig0a.other[0] -> other +DLGLOBAL DEBUG test(other){alive}: Received Event EV_OTHER_GONE +DLGLOBAL DEBUG 2 (__twig0a.cleanup(),other.alive()) +DLGLOBAL DEBUG test(other){alive}: alive(EV_OTHER_GONE) +DLGLOBAL DEBUG 3 (__twig0a.cleanup(),other.alive(),other.other_gone()) +DLGLOBAL DEBUG 2 (__twig0a.cleanup(),other.alive()) +DLGLOBAL DEBUG 1 (__twig0a.cleanup()) +DLGLOBAL DEBUG test(_branch0){alive}: Received Event EV_CHILD_GONE +DLGLOBAL DEBUG 2 (__twig0a.cleanup(),_branch0.alive()) +DLGLOBAL DEBUG test(_branch0){alive}: alive(EV_CHILD_GONE) +DLGLOBAL DEBUG 3 (__twig0a.cleanup(),_branch0.alive(),_branch0.child_gone()) +DLGLOBAL DEBUG test(_branch0){alive}: EV_CHILD_GONE: Dropped reference _branch0.child[0] = __twig0a +DLGLOBAL DEBUG test(_branch0){alive}: No more children +DLGLOBAL DEBUG 2 (__twig0a.cleanup(),_branch0.alive()) +DLGLOBAL DEBUG test(_branch0){alive}: Ignoring trigger to terminate: already terminating +DLGLOBAL DEBUG 1 (__twig0a.cleanup()) +DLGLOBAL DEBUG test(__twig0a){alive}: cleanup() done +DLGLOBAL DEBUG 0 (-) +DLGLOBAL DEBUG test(__twig0a){alive}: Freeing instance +DLGLOBAL DEBUG test(__twig0a){alive}: Deallocated +DLGLOBAL DEBUG test(_branch0){alive}: Removing from parent test(root) +DLGLOBAL DEBUG 1 (_branch0.cleanup()) +DLGLOBAL DEBUG test(_branch0){alive}: cleanup() +DLGLOBAL DEBUG test(_branch0){alive}: scene forgets _branch0 +DLGLOBAL DEBUG test(_branch0){alive}: removing reference _branch0.other[0] -> other +DLGLOBAL DEBUG test(other){alive}: Received Event EV_OTHER_GONE +DLGLOBAL DEBUG 2 (_branch0.cleanup(),other.alive()) +DLGLOBAL DEBUG test(other){alive}: alive(EV_OTHER_GONE) +DLGLOBAL DEBUG 3 (_branch0.cleanup(),other.alive(),other.other_gone()) +DLGLOBAL DEBUG test(other){alive}: EV_OTHER_GONE: Dropped reference other.other[0] = _branch0 +DLGLOBAL DEBUG 2 (_branch0.cleanup(),other.alive()) +DLGLOBAL DEBUG test(other){alive}: Terminating (cause = OSMO_FSM_TERM_REGULAR) +DLGLOBAL DEBUG test(other){alive}: pre_term() +DLGLOBAL DEBUG 3 (_bra | 
