github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/interactive/interactive_client_init.go (about)

     1  package interactive
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"log"
     7  	"time"
     8  
     9  	"github.com/spf13/viper"
    10  	"github.com/turbot/go-kit/helpers"
    11  	"github.com/turbot/steampipe/pkg/constants"
    12  	"github.com/turbot/steampipe/pkg/db/db_common"
    13  	"github.com/turbot/steampipe/pkg/error_helpers"
    14  	"github.com/turbot/steampipe/pkg/statushooks"
    15  	"github.com/turbot/steampipe/pkg/workspace"
    16  )
    17  
    18  // init data has arrived, handle any errors/warnings/messages
    19  func (c *InteractiveClient) handleInitResult(ctx context.Context, initResult *db_common.InitResult) {
    20  	// whatever happens, set initialisationComplete
    21  	defer func() {
    22  		c.initialisationComplete = true
    23  	}()
    24  
    25  	if initResult.Error != nil {
    26  		c.ClosePrompt(AfterPromptCloseExit)
    27  		// add newline to ensure error is not printed at end of current prompt line
    28  		fmt.Println()
    29  		c.promptResult.PromptErr = initResult.Error
    30  		return
    31  	}
    32  
    33  	if error_helpers.IsContextCanceled(ctx) {
    34  		c.ClosePrompt(AfterPromptCloseExit)
    35  		// add newline to ensure error is not printed at end of current prompt line
    36  		fmt.Println()
    37  		error_helpers.ShowError(ctx, initResult.Error)
    38  		log.Printf("[TRACE] prompt context has been cancelled - not handling init result")
    39  		return
    40  	}
    41  
    42  	if initResult.HasMessages() {
    43  		c.showMessages(ctx, initResult.DisplayMessages)
    44  	}
    45  
    46  	// initialise autocomplete suggestions
    47  	//nolint:golint,errcheck // worst case is we won't have autocomplete - this is not a failure
    48  	c.initialiseSuggestions(ctx)
    49  	// tell the workspace to reset the prompt after displaying async filewatcher messages
    50  	c.initData.Workspace.SetOnFileWatcherEventMessages(func() {
    51  		//nolint:golint,errcheck // worst case is we won't have autocomplete - this is not a failure
    52  		c.initialiseSuggestions(ctx)
    53  		c.interactivePrompt.Render()
    54  	})
    55  
    56  }
    57  
    58  func (c *InteractiveClient) showMessages(ctx context.Context, showMessages func()) {
    59  	statushooks.Done(ctx)
    60  	// clear the prompt
    61  	// NOTE: this must be done BEFORE setting hidePrompt
    62  	// otherwise the cursor calculations in go-prompt do not work and multi-line test is not cleared
    63  	c.interactivePrompt.ClearLine()
    64  	// set the flag hide the prompt prefix in the next prompt render cycle
    65  	c.hidePrompt = true
    66  	// call ClearLine to render the empty prefix
    67  	c.interactivePrompt.ClearLine()
    68  
    69  	// call the passed in func to display the messages
    70  	showMessages()
    71  
    72  	// show the prompt again
    73  	c.hidePrompt = false
    74  
    75  	// We need to render the prompt here to make sure that it comes back
    76  	// after the messages have been displayed (only if there's no execution)
    77  	//
    78  	// We check for query execution by TRYING to acquire the same lock that
    79  	// execution locks on
    80  	//
    81  	// If we can acquire a lock, that means that there's no
    82  	// query execution underway - and it is safe to render the prompt
    83  	//
    84  	// otherwise, that query execution is waiting for this init to finish
    85  	// and as such will be out of the prompt - in which case, we shouldn't
    86  	// re-render the prompt
    87  	//
    88  	// the prompt will be re-rendered when the query execution finished
    89  	if c.executionLock.TryLock() {
    90  		c.interactivePrompt.Render()
    91  		// release the lock
    92  		c.executionLock.Unlock()
    93  	}
    94  }
    95  
    96  func (c *InteractiveClient) readInitDataStream(ctx context.Context) {
    97  	defer func() {
    98  		if r := recover(); r != nil {
    99  			c.interactivePrompt.ClearScreen()
   100  			error_helpers.ShowError(ctx, helpers.ToError(r))
   101  		}
   102  	}()
   103  
   104  	<-c.initData.Loaded
   105  
   106  	defer func() { c.initResultChan <- c.initData.Result }()
   107  
   108  	if c.initData.Result.Error != nil {
   109  		return
   110  	}
   111  	statushooks.SetStatus(ctx, "Load plugin schemas…")
   112  	//  fetch the schema
   113  	// TODO make this async https://github.com/turbot/steampipe/issues/3400
   114  	// NOTE: we would like to do this asyncronously, but we are currently limited to a single Db connection in our
   115  	// as the client cache settings are set per connection so we rely on only having a single connection
   116  	// This means that the schema load would block other queries anyway so there is no benefit right not in making asyncronous
   117  
   118  	if err := c.loadSchema(); err != nil {
   119  		c.initData.Result.Error = err
   120  		return
   121  	}
   122  
   123  	log.Printf("[TRACE] SetupWatcher")
   124  
   125  	statushooks.SetStatus(ctx, "Start file watcher…")
   126  	// start the workspace file watcher
   127  	if viper.GetBool(constants.ArgWatch) {
   128  		// provide an explicit error handler which re-renders the prompt after displaying the error
   129  		if err := c.initData.Workspace.SetupWatcher(ctx, c.initData.Client, c.workspaceWatcherErrorHandler); err != nil {
   130  			c.initData.Result.Error = err
   131  		}
   132  	}
   133  
   134  	statushooks.SetStatus(ctx, "Start notifications listener…")
   135  	log.Printf("[TRACE] Start notifications listener")
   136  
   137  	// subscribe to postgres notifications
   138  	statushooks.SetStatus(ctx, "Subscribe to postgres notifications…")
   139  
   140  	c.listenToPgNotifications(ctx)
   141  }
   142  
   143  func (c *InteractiveClient) workspaceWatcherErrorHandler(ctx context.Context, err error) {
   144  	fmt.Println()
   145  	error_helpers.ShowError(ctx, err)
   146  	c.interactivePrompt.Render()
   147  }
   148  
   149  // return whether the client is initialises
   150  // there are 3 conditions>
   151  func (c *InteractiveClient) isInitialised() bool {
   152  	return c.initialisationComplete
   153  }
   154  
   155  func (c *InteractiveClient) waitForInitData(ctx context.Context) error {
   156  	var initTimeout = 40 * time.Second
   157  	ticker := time.NewTicker(20 * time.Millisecond)
   158  	for {
   159  		select {
   160  		case <-ctx.Done():
   161  			return ctx.Err()
   162  		case <-ticker.C:
   163  			if c.isInitialised() {
   164  				// if there was an error in initialisation, return it
   165  				return c.initData.Result.Error
   166  			}
   167  		case <-time.After(initTimeout):
   168  			return fmt.Errorf("timed out waiting for initialisation to complete")
   169  		}
   170  	}
   171  }
   172  
   173  // return the workspace, or nil if not yet initialised
   174  func (c *InteractiveClient) workspace() *workspace.Workspace {
   175  	if c.initData == nil {
   176  		return nil
   177  	}
   178  	return c.initData.Workspace
   179  }
   180  
   181  // return the client, or nil if not yet initialised
   182  func (c *InteractiveClient) client() db_common.Client {
   183  	if c.initData == nil {
   184  		return nil
   185  	}
   186  	return c.initData.Client
   187  }