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 }