github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/task/runner.go (about) 1 package task 2 3 import ( 4 "context" 5 "fmt" 6 "log" 7 "os" 8 "sync" 9 "time" 10 11 "github.com/spf13/cobra" 12 "github.com/turbot/go-kit/files" 13 "github.com/turbot/steampipe/pkg/db/db_local" 14 "github.com/turbot/steampipe/pkg/error_helpers" 15 "github.com/turbot/steampipe/pkg/filepaths" 16 "github.com/turbot/steampipe/pkg/installationstate" 17 "github.com/turbot/steampipe/pkg/plugin" 18 "github.com/turbot/steampipe/pkg/utils" 19 ) 20 21 const minimumDurationBetweenChecks = 24 * time.Hour 22 23 type Runner struct { 24 currentState installationstate.InstallationState 25 options *taskRunConfig 26 } 27 28 // RunTasks runs all tasks asynchronously 29 // returns a channel which is closed once all tasks are finished or the provided context is cancelled 30 func RunTasks(ctx context.Context, cmd *cobra.Command, args []string, options ...TaskRunOption) chan struct{} { 31 utils.LogTime("task.RunTasks start") 32 defer utils.LogTime("task.RunTasks end") 33 34 config := newRunConfig() 35 for _, o := range options { 36 o(config) 37 } 38 39 doneChannel := make(chan struct{}, 1) 40 runner := newRunner(config) 41 42 // if there are any notifications from the previous run - display them 43 if err := runner.displayNotifications(cmd, args); err != nil { 44 log.Println("[TRACE] faced error displaying notifications:", err) 45 } 46 47 // asynchronously run the task runner 48 go func(c context.Context) { 49 defer close(doneChannel) 50 // check if a legacy notifications file exists 51 exists := files.FileExists(filepaths.LegacyNotificationsFilePath()) 52 if exists { 53 log.Println("[TRACE] found legacy notification file. removing") 54 // if the legacy file exists, remove it 55 os.Remove(filepaths.LegacyNotificationsFilePath()) 56 } 57 58 // if the legacy file existed, then we should enforce a run, since we need 59 // to update the available version cache 60 if runner.shouldRun() || exists { 61 for _, hook := range config.preHooks { 62 hook(c) 63 } 64 runner.run(c) 65 } 66 }(ctx) 67 68 return doneChannel 69 } 70 71 func newRunner(config *taskRunConfig) *Runner { 72 utils.LogTime("task.NewRunner start") 73 defer utils.LogTime("task.NewRunner end") 74 75 r := new(Runner) 76 r.options = config 77 78 state, err := installationstate.Load() 79 if err != nil { 80 // this error should never happen 81 // log this and carry on 82 log.Println("[TRACE] error loading state,", err) 83 } 84 r.currentState = state 85 return r 86 } 87 88 func (r *Runner) run(ctx context.Context) { 89 utils.LogTime("task.Runner.Run start") 90 defer utils.LogTime("task.Runner.Run end") 91 92 var availableCliVersion *CLIVersionCheckResponse 93 var availablePluginVersions map[string]plugin.VersionCheckReport 94 95 waitGroup := sync.WaitGroup{} 96 97 if r.options.runUpdateCheck { 98 // check whether an updated version is available 99 r.runJobAsync(ctx, func(c context.Context) { 100 availableCliVersion, _ = fetchAvailableCLIVersion(ctx, r.currentState.InstallationID) 101 }, &waitGroup) 102 103 // check whether an updated version is available 104 r.runJobAsync(ctx, func(c context.Context) { 105 availablePluginVersions = plugin.GetAllUpdateReport(c, r.currentState.InstallationID) 106 }, &waitGroup) 107 } 108 109 // remove log files older than 7 days 110 r.runJobAsync(ctx, func(_ context.Context) { db_local.TrimLogs() }, &waitGroup) 111 112 // wait for all jobs to complete 113 waitGroup.Wait() 114 115 // check if the context was cancelled before starting any FileIO 116 if error_helpers.IsContextCanceled(ctx) { 117 // if the context was cancelled, we don't want to do anything 118 return 119 } 120 121 // save the notifications, if any 122 if err := r.saveAvailableVersions(availableCliVersion, availablePluginVersions); err != nil { 123 error_helpers.ShowWarning(fmt.Sprintf("Regular task runner failed to save pending notifications: %s", err)) 124 } 125 126 // save the state - this updates the last checked time 127 if err := r.currentState.Save(); err != nil { 128 error_helpers.ShowWarning(fmt.Sprintf("Regular task runner failed to save state file: %s", err)) 129 } 130 } 131 132 func (r *Runner) runJobAsync(ctx context.Context, job func(context.Context), wg *sync.WaitGroup) { 133 wg.Add(1) 134 go func() { 135 // do this as defer, so that it always fires - even if there's a panic 136 defer wg.Done() 137 job(ctx) 138 }() 139 } 140 141 // determines whether the task runner should run at all 142 // tasks are to be run at most once every 24 hours 143 func (r *Runner) shouldRun() bool { 144 utils.LogTime("task.Runner.shouldRun start") 145 defer utils.LogTime("task.Runner.shouldRun end") 146 147 now := time.Now() 148 if r.currentState.LastCheck == "" { 149 return true 150 } 151 lastCheckedAt, err := time.Parse(time.RFC3339, r.currentState.LastCheck) 152 if err != nil { 153 return true 154 } 155 durationElapsedSinceLastCheck := now.Sub(lastCheckedAt) 156 157 return durationElapsedSinceLastCheck > minimumDurationBetweenChecks 158 } 159 160 func showNotificationsForCommand(cmd *cobra.Command, cmdArgs []string) bool { 161 return !(isPluginUpdateCmd(cmd) || 162 IsPluginManagerCmd(cmd) || 163 isServiceStopCmd(cmd) || 164 IsBatchQueryCmd(cmd, cmdArgs) || 165 isCompletionCmd(cmd) || 166 isPluginListCmd(cmd)) 167 } 168 169 func isServiceStopCmd(cmd *cobra.Command) bool { 170 return cmd.Parent() != nil && cmd.Parent().Name() == "service" && cmd.Name() == "stop" 171 } 172 func isCompletionCmd(cmd *cobra.Command) bool { 173 return cmd.Name() == "completion" 174 } 175 func IsPluginManagerCmd(cmd *cobra.Command) bool { 176 return cmd.Name() == "plugin-manager" 177 } 178 func isPluginUpdateCmd(cmd *cobra.Command) bool { 179 return cmd.Name() == "update" && cmd.Parent() != nil && cmd.Parent().Name() == "plugin" 180 } 181 func IsBatchQueryCmd(cmd *cobra.Command, cmdArgs []string) bool { 182 return cmd.Name() == "query" && len(cmdArgs) > 0 183 } 184 func isPluginListCmd(cmd *cobra.Command) bool { 185 return cmd.Name() == "list" && cmd.Parent() != nil && cmd.Parent().Name() == "plugin" 186 } 187 188 func IsCheckCmd(cmd *cobra.Command) bool { 189 return cmd.Name() == "check" 190 } 191 192 func IsDashboardCmd(cmd *cobra.Command) bool { 193 return cmd.Name() == "dashboard" 194 } 195 196 func IsModCmd(cmd *cobra.Command) bool { 197 parent := cmd.Parent() 198 return parent.Name() == "mod" 199 }