Files
steampipe/cmd/root.go
Nathan Wallace 46809b7982 Fix #4708: Thread-safe AddCommands/ResetCommands with mutex (#4904)
* Add test demonstrating race condition in AddCommands/ResetCommands

Added TestAddCommands_Concurrent which exposes data races when
AddCommands() and ResetCommands() are called concurrently.
Running with -race flag shows multiple race condition warnings.

This test demonstrates bug #4708 where these functions are not
thread-safe. While not a practical issue (only called during
single-threaded CLI initialization), proper synchronization
should be added.

Related to #4708

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix race condition in AddCommands/ResetCommands with mutex

Added thread-safe synchronization to AddCommands() and created a
new thread-safe ResetCommands() wrapper using a shared mutex.

The underlying cobra.Command methods are not thread-safe, causing
data races when called concurrently. While these functions are
typically only called during single-threaded CLI initialization,
adding proper synchronization ensures correctness and allows for
safe concurrent usage in tests.

Changes:
- Added commandMutex to protect concurrent access to rootCmd
- Updated AddCommands() with mutex lock/unlock
- Created ResetCommands() wrapper with mutex protection
- Updated test to use the new thread-safe ResetCommands()

Test now passes with -race flag with no warnings.

Fixes #4708

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-17 04:04:22 -05:00

148 lines
4.4 KiB
Go

package cmd
import (
"context"
"os"
"sync"
"github.com/mattn/go-isatty"
"github.com/spf13/cobra"
"github.com/spf13/viper"
filehelpers "github.com/turbot/go-kit/files"
"github.com/turbot/pipe-fittings/v2/app_specific"
"github.com/turbot/pipe-fittings/v2/constants"
"github.com/turbot/pipe-fittings/v2/utils"
"github.com/turbot/steampipe/v2/pkg/error_helpers"
"github.com/turbot/steampipe/v2/pkg/statushooks"
)
var exitCode int
// commandMutex protects concurrent access to rootCmd's command list
var commandMutex sync.Mutex
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "steampipe [--version] [--help] COMMAND [args]",
Short: "Query cloud resources using SQL",
Long: `Steampipe: select * from cloud;
Dynamically query APIs, code and more with SQL.
Zero-ETL from 140+ data sources.
Common commands:
# Interactive SQL query console
steampipe query
# Install a plugin from the hub - https://hub.steampipe.io
steampipe plugin install aws
# Execute a defined SQL query
steampipe query "select * from aws_s3_bucket"
# Get help for a command
steampipe help query
Documentation: https://steampipe.io/docs
`,
}
func InitCmd() {
utils.LogTime("cmd.root.InitCmd start")
defer utils.LogTime("cmd.root.InitCmd end")
defaultInstallDir, err := filehelpers.Tildefy(app_specific.DefaultInstallDir)
error_helpers.FailOnError(err)
// Set the version after viper has been initialized
rootCmd.Version = viper.GetString("main.version")
rootCmd.SetVersionTemplate("Steampipe v{{.Version}}\n")
// global flags
rootCmd.PersistentFlags().String(constants.ArgWorkspaceProfile, "default", "The workspace profile to use") // workspace profile profile is a global flag since install-dir(global) can be set through the workspace profile
rootCmd.PersistentFlags().String(constants.ArgInstallDir, defaultInstallDir, "Path to the Config Directory")
rootCmd.PersistentFlags().Bool(constants.ArgSchemaComments, true, "Include schema comments when importing connection schemas")
error_helpers.FailOnError(viper.BindPFlag(constants.ArgInstallDir, rootCmd.PersistentFlags().Lookup(constants.ArgInstallDir)))
error_helpers.FailOnError(viper.BindPFlag(constants.ArgWorkspaceProfile, rootCmd.PersistentFlags().Lookup(constants.ArgWorkspaceProfile)))
error_helpers.FailOnError(viper.BindPFlag(constants.ArgSchemaComments, rootCmd.PersistentFlags().Lookup(constants.ArgSchemaComments)))
AddCommands()
// disable auto completion generation, since we don't want to support
// powershell yet - and there's no way to disable powershell in the default generator
rootCmd.CompletionOptions.DisableDefaultCmd = true
rootCmd.Flags().BoolP(constants.ArgHelp, "h", false, "Help for steampipe")
hideRootFlags(constants.ArgSchemaComments)
// tell OS to reclaim memory immediately
os.Setenv("GODEBUG", "madvdontneed=1")
}
func hideRootFlags(flags ...string) {
for _, flag := range flags {
if f := rootCmd.Flag(flag); f != nil {
f.Hidden = true
}
}
}
// AddCommands adds all subcommands to the root command.
//
// This function is thread-safe and can be called concurrently.
// However, it is typically only called during CLI initialization
// in a single-threaded context.
func AddCommands() {
commandMutex.Lock()
defer commandMutex.Unlock()
// explicitly initialise commands here rather than in init functions to allow us to handle errors from the config load
rootCmd.AddCommand(
pluginCmd(),
queryCmd(),
serviceCmd(),
generateCompletionScriptsCmd(),
pluginManagerCmd(),
loginCmd(),
)
}
// ResetCommands removes all subcommands from the root command.
//
// This function is thread-safe and can be called concurrently.
// It is primarily used for testing.
func ResetCommands() {
commandMutex.Lock()
defer commandMutex.Unlock()
rootCmd.ResetCommands()
}
func Execute() int {
utils.LogTime("cmd.root.Execute start")
defer utils.LogTime("cmd.root.Execute end")
ctx := createRootContext()
err := rootCmd.ExecuteContext(ctx)
if err != nil {
exitCode = 1
}
return exitCode
}
// create the root context - add a status renderer
func createRootContext() context.Context {
statusRenderer := statushooks.NullHooks
// if the client is a TTY, inject a status spinner
if isatty.IsTerminal(os.Stdout.Fd()) {
statusRenderer = statushooks.NewStatusSpinnerHook()
}
ctx := statushooks.AddStatusHooksToContext(context.Background(), statusRenderer)
return ctx
}