Skip to content
Snippets Groups Projects
plpgsql_debugger.c 40.2 KiB
Newer Older
  • Learn to ignore specific revisions
  • 	 * assign_expr when we need to deposit a value in variable.
    	 */
    	dbg_info->error_callback = plugin_funcs.error_callback;
    	dbg_info->assign_expr    = plugin_funcs.assign_expr;
    
    #if INCLUDE_PACKAGE_SUPPORT
    	/*
    	 * Look up the package this function belongs to.
    	 *
    	 * Inline code blocks have invalid fn_oid. They never belong to packages.
    	 */
    	if (OidIsValid(dbg_info->func->fn_oid))
    	{
    		/*
    		 * Find the namespace in which this function/procedure is defined
    		 */
    		HeapTuple	htup;
    		Oid			namespaceOid;
    
    		htup = SearchSysCache(PROCOID, ObjectIdGetDatum(dbg_info->func->fn_oid), 0, 0, 0);
    
    		if (!HeapTupleIsValid(htup))
    			elog(ERROR, "cache lookup failed for procedure %d", dbg_info->func->fn_oid);
    
    		namespaceOid = ((Form_pg_proc)GETSTRUCT(htup))->pronamespace;
    
    		ReleaseSysCache(htup);
    
    		/*
    		 * Now figure out if this namespace is a package or a schema
    		 *
    		 * NOTE: we could read the pg_namespace tuple and check pg_namespace.nspparent,
    		 *		 but it's faster to just search for the namespaceOid in the global
    		 *		 package array instead; we have to do that anyway to find the package
    		 */
    
    		dbg_info->package = plugin_funcs.get_package( namespaceOid );
    	}
    
    #endif
    }
    
    
    /*
     * ---------------------------------------------------------------------
    
    Robert Haas's avatar
    Robert Haas committed
     * plpgsql_do_deposit()
    
     *
     *  This function handles the 'deposit' feature - that is, this function
     *  sets a given PL variable to a new value, supplied by the client.
     *
    
     *	do_deposit() is called when you type a new value into a variable in
    
     *  NOTE: For the convenience of the user, we first assume that the
    
     *		  provided value is an expression.  If it doesn't evaluate,
     *		  we convert the value into a literal by surrounding it with
     *		  single quotes.  That may be surprising if you happen to make
     *		  a typo, but it will "do the right thing" in most cases.
    
     *
     * Returns true on success, false on failure.
    
    Robert Haas's avatar
    Robert Haas committed
    static bool
    plpgsql_do_deposit(ErrorContextCallback *frame, const char *var_name,
    				   int lineno, const char *value)
    
    	PLpgSQL_execstate *estate = (PLpgSQL_execstate *) frame->arg;
    	dbg_ctx       *dbg_info   = estate->plugin_info;
    	PLpgSQL_datum *target;
    
    	char      	  *select;
    	PLpgSQL_expr  *expr;
    	MemoryContext  curContext = CurrentMemoryContext;
    	ResourceOwner  curOwner   = CurrentResourceOwner;
    
    	target = find_datum_by_name(estate, var_name, lineno, NULL);
    
    	if (!target)
    
    
    	/*
    	 * Now build a SELECT statement that returns the requested value
    	 *
    	 * NOTE: we allocate 2 extra bytes for quoting just in case we
    	 *       need to later (see the retry logic below)
    	 */
    
    	select = palloc( strlen( "SELECT " ) + strlen( value ) + 2 + 1 );
    
    	sprintf( select, "SELECT %s", value );
    
    
    	 * Note: we must create a dynamically allocated PLpgSQL_expr here - we
    
    	 *       can't create one on the stack because exec_assign_expr()
    	 *       links this expression into a list (active_simple_exprs) and
    
    	 *       this expression must survive until the end of the current
    
    	 *	     transaction so we don't free it out from under spl_plpgsql_xact_cb()
    	 */
    
    
    	expr = (PLpgSQL_expr *) palloc0( sizeof( *expr ));
    
    #if (PG_VERSION_NUM < 110000)
    	expr->dtype			= PLPGSQL_DTYPE_EXPR;
    	expr->dno			= -1;
    #endif
    	expr->query			= select;
    	expr->plan			= NULL;
    
    	expr->plan_argtypes		= NULL;
    	expr->nparams			= 0;
    
    	expr->expr_simple_expr		= NULL;
    
    
    	BeginInternalSubTransaction( NULL );
    
    	MemoryContextSwitchTo( curContext );
    
    	PG_TRY();
    	{
    		if( target )
    
    			dbg_info->assign_expr( estate, target, expr );
    
    
    		/* Commit the inner transaction, return to outer xact context */
    		ReleaseCurrentSubTransaction();
    		MemoryContextSwitchTo( curContext );
    		CurrentResourceOwner = curOwner;
    
    
    		/* That worked, don't try again */
    		retval = true;
    
    		MemoryContextSwitchTo( curContext );
    
    		FlushErrorState();
    
    		/* Abort the inner transaction */
    		RollbackAndReleaseCurrentSubTransaction();
    		MemoryContextSwitchTo( curContext );
    		CurrentResourceOwner = curOwner;
    
    
    		/* That failed - try again as a literal */
    		retval = false;
    
    	 * If the given value is not a valid expression, try converting
    	 * the value into a literal by sinqle-quoting it.
    	 */
    
    
    #if (PG_VERSION_NUM < 110000)
    		expr->dtype		= PLPGSQL_DTYPE_EXPR;
    		expr->dno		= -1;
    #endif
    		expr->query		= select;
    		expr->plan		= NULL;
    		expr->expr_simple_expr	= NULL;
    
    		expr->plan_argtypes	= NULL;
    		expr->nparams		= 0;
    
    #endif
    
    		BeginInternalSubTransaction( NULL );
    
    		MemoryContextSwitchTo( curContext );
    
    		PG_TRY();
    		{
    			if( target )
    
    				dbg_info->assign_expr( estate, target, expr );
    
    
    			/* Commit the inner transaction, return to outer xact context */
    			ReleaseCurrentSubTransaction();
    			MemoryContextSwitchTo( curContext );
    			CurrentResourceOwner = curOwner;
    
    
    			MemoryContextSwitchTo( curContext );
    
    			FlushErrorState();
    
    			/* Abort the inner transaction */
    			RollbackAndReleaseCurrentSubTransaction();
    			MemoryContextSwitchTo( curContext );
    			CurrentResourceOwner = curOwner;
    
    
    }
    
    /*
     * ---------------------------------------------------------------------
     * is_datum_visible()
     *
    
     *	This function determines whether the given datum is 'visible' to the
     *  debugger client.  We want to hide a few undocumented/internally
    
     *  generated variables from the user - this is the function that hides
     *  them.  We set a flag in the symbols entry for this datum
     *  to indicate whether this variable is hidden or visible - that way,
     *  only have to do the expensive stuff once per invocation.
     */
    
    Robert Haas's avatar
    Robert Haas committed
    static bool
    is_datum_visible(PLpgSQL_datum *datum)
    
    	static const char * hidden_variables[] =
    
    	 * All of the hidden variables are scalars at the moment so
    
    	 * assume that anything else is visible regardless of name
    	 */
    
    	if( datum->dtype != PLPGSQL_DTYPE_VAR )
    		return( TRUE );
    	else
    	{
    		PLpgSQL_var * var = (PLpgSQL_var *)datum;
    		int			  i;
    
    		for( i = 0; i < sizeof( hidden_variables ) / sizeof( hidden_variables[0] ); ++i )
    		{
    			if( strcmp( var->refname, hidden_variables[i] ) == 0 )
    			{
    				/*
    				 * We found this variable in our list of hidden names -
    				 * this variable is *not* visible
    				 */
    
    				return( FALSE );
    			}
    		}
    
    		/*
    
    		 * The SPL pre-processor generates a few variable names for
    
    		 * DMBS.PUTLINE statements - we want to hide those variables too.
    
    		 * The generated variables are of the form 'txtnnn...' where
    
    		 * 'nnn...' is a sequence of one or more digits.
    		 */
    
    		if( strncmp( var->refname, "txt", 3 ) == 0 )
    		{
    			int	   i;
    
    			/*
    			 * Starts with 'txt' - see if the rest of the string is composed
    			 * entirely of digits
    			 */
    
    			for( i = 3; var->refname[i] != '\0'; ++i )
    			{
    				if( var->refname[i] < '0' || var->refname[i] > '9' )
    					return( TRUE );
    			}
    
    			return( FALSE );
    		}
    
    		return( TRUE );
    	}
    }
    
    /*
     * ---------------------------------------------------------------------
     * is_var_visible()
     *
    
     *	This function determines whether the given variable is 'visible' to the
     *  debugger client. We hide some variables from the user (see the
     *  is_datum_visible() function for more info).  This function is quick -
    
     *  we do the slow work in is_datum_visible() and simply check the results
     *  here.
     */
    
    Robert Haas's avatar
    Robert Haas committed
    static bool
    is_var_visible(PLpgSQL_execstate *frame, int var_no)
    
    	dbg_ctx * dbg_info = (dbg_ctx *)frame->plugin_info;
    
    
    	if (dbg_info->symbols == NULL)
    		completeFrame(frame);
    
    	return( dbg_info->symbols[var_no].visible );
    }
    
    
    /*
    
     * This function sends the current position to the debugger client. We
    
     * send the function's OID, xmin, cmin, and the current line number
    
     * (we're telling the client which line of code we're about to execute).
    
    static void
    plpgsql_send_cur_line(ErrorContextCallback *frame)
    
    	PLpgSQL_execstate *estate = (PLpgSQL_execstate *) frame->arg;
    	PLpgSQL_stmt *stmt = estate->err_stmt;
    
    	dbg_ctx	   *dbg_info = (dbg_ctx *) estate->plugin_info;
    
    	PLpgSQL_function *func = dbg_info->func;
    
    
    	dbg_send( "%d:%d:%s",
    			  func->fn_oid,
    			  stmt->lineno+1,
    #if (PG_VERSION_NUM >= 90200)
    			  func->fn_signature
    #else
    			  func->fn_name
    #endif
    		);
    }
    
    /*
     * ---------------------------------------------------------------------
     * isFirstStmt()
     *
    
     *	Returns true if the given statement is the first statement in the
    
    Robert Haas's avatar
    Robert Haas committed
    static bool
    isFirstStmt(PLpgSQL_stmt *stmt, PLpgSQL_function *func)
    
    {
    	if( stmt == linitial( func->action->body ))
    		return( TRUE );
    	else
    		return( FALSE );
    }
    
    /*
     * ---------------------------------------------------------------------
     * dbg_newstmt()
     *
     *	The PL/pgSQL executor calls plpgsql_dbg_newstmt() just before executing each
    
     *	statement.
    
     *
     *	This function is the heart of the debugger.  If you're single-stepping,
     *	or you hit a breakpoint, plpgsql_dbg_newstmt() sends a message to the debugger
    
     *  client indicating the current line and then waits for a command from
    
     *	NOTE: it is very important that this function should impose negligible
    
     *	      overhead when a debugger client is *not* attached.  In other words
     *		  if you're running PL/pgSQL code without a debugger, you notice no
     *		  performance penalty.
     */
    
    Robert Haas's avatar
    Robert Haas committed
    static void
    dbg_newstmt(PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt)
    
    	 * If there's no debugger attached, go home as quickly as possible.
    	 */
    	if( frame->plugin_info == NULL )
    		return;
    	else
    	{
    		dbg_ctx 		  * dbg_info = (dbg_ctx *)frame->plugin_info;
    		Breakpoint		  * breakpoint = NULL;
    		eBreakpointScope	breakpointScope = 0;
    
    		/*
    		 * The PL compiler marks certain statements as 'invisible' to the
    		 * debugger. In particular, the compiler may generate statements
    		 * that do not appear in the source code. Such a statement is
    		 * marked with a line number of -1: if we're looking at an invisible
    		 * statement, just return to the caller.
    		 */
    
    		if( stmt->lineno == -1 )
    			return;
    
    		/*
    		 * Now set up an error handler context so we can intercept any
    		 * networking errors (errors communicating with the proxy).
    		 */
    
    		if( sigsetjmp( client_lost.m_savepoint, 1 ) != 0 )
    		{
    			/*
    
    			 *  The connection to the debugger client has slammed shut -
    
    			 *	just pretend like there's no debugger attached and return
    			 *
    			 *	NOTE: we no longer have a connection to the debugger proxy -
    			 *		  that means that we cannot interact with the proxy, we
    			 *		  can't wait for another command, nothing.  We let the
    			 *		  executor continue execution - anything else will hang
    			 *		  this backend, waiting for a debugger command that will
    			 *		  never arrive.
    			 *
    			 *		  If, however, we hit a breakpoint again, we'll stop and
    			 *		  wait for another debugger proxy to connect to us.  If
    
    			 *		  that's not the behavior you're looking for, you can
    
    			 *		  drop the breakpoint, or call free_function_breakpoints()
    			 *		  here to get rid of all breakpoints in this backend.
    			 */
    			per_session_ctx.client_w = 0; 		/* No client connection */
    			dbg_info->stepping 		 = FALSE; 	/* No longer stepping   */
    		}
    
    		if(( dbg_info->stepping ) || breakAtThisLine( &breakpoint, &breakpointScope, dbg_info->func->fn_oid, isFirstStmt( stmt, dbg_info->func ) ? -1 : stmt->lineno ))
    			dbg_info->stepping = TRUE;
    		else
    			return;
    
    		per_session_ctx.step_into_next_func = FALSE;
    
    		/* We found a breakpoint for this function (or we're stepping into) */
    		/* Make contact with the debugger client */
    
    		if( !attach_to_proxy( breakpoint ))
    		{
    
    			 * Can't attach to the proxy, maybe we found a stale breakpoint?
    			 * That can happen if you set a global breakpoint on a function,
    			 * invoke that function from a client application, debug the target
    			 * kill the debugger client, and then re-invoke the function from
    			 * the same client application - we will find the stale global
    			 * breakpoint on the second invocation.
    			 *
    			 * We want to remove that breakpoint so that we don't keep trying
    
    			 * to attach to a phantom proxy process.
    
    			 */
    			if( breakpoint )
    				BreakpointDelete( breakpointScope, &(breakpoint->key));
    
    			/*
    
    			 * In any case, if we don't have a proxy to work with, we can't
    
    			 * do any debugging so give up.
    			 */
    			pfree( frame->plugin_info );
    			frame->plugin_info       = NULL; /* No debugger context  */
    			per_session_ctx.client_w = 0; /* No client connection */
    
    			return;
    		}
    
    		if( stmt->cmd_type == PLPGSQL_STMT_BLOCK )
    			return;
    
    		/*
    
    		 * The PL/pgSQL compiler inserts an automatic RETURN statement at the
    
    		 * end of each function (unless the last statement in the function is
    		 * already a RETURN). If we run into that statement, we don't really
    		 * want to wait for the user to STEP across it. Remember, the user won't
    		 * see the RETURN statement in the source-code listing for his function.
    		 *
    
    		 * Fortunately, the automatic RETURN statement has a line-number of 0
    
    		 * If we're in step mode, tell the debugger client, read a command from the client and
    
    		 * execute the command
    		 */
    
    		if( dbg_info->stepping )
    		{
    			/*
    			 * Make sure that we have all of the debug info that we need in this stack frame
    			 */
    			completeFrame( frame );
    
    
    			/*
    			 * We're in single-step mode (or at a breakpoint)
    
    			 * send the current line number to the debugger client and report any
    			 * variable modifications
    			 */
    
    
    			if (!plugin_debugger_main_loop())
    				dbg_info->stepping = FALSE;
    
    		}
    	}
    }
    
    /* ---------------------------------------------------------------------
     *	datumIsNull()
    
     *	determine whether datum is NULL or not.
     *	TODO: consider datatypes other than PLPGSQL_DTYPE_VAR as well
     */
    
    Robert Haas's avatar
    Robert Haas committed
    static bool
    datumIsNull(PLpgSQL_datum *datum)
    
    {
    	switch (datum->dtype)
    	{
    		case PLPGSQL_DTYPE_VAR:
    		{
    			PLpgSQL_var *var = (PLpgSQL_var *) datum;
    
    
    		/* other data types are not currently handled, we just return true */
    
    		case PLPGSQL_DTYPE_REC:
    		case PLPGSQL_DTYPE_ROW:
    			return true;