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  }