Files
alda-mirror/client/cmd/root.go
Dave Yarwood d523334d99 Use http://slack.alda.io for now, as the https:// version isn't working
This is safe to do because that page simply redirects to another page whose URL
begins with https://.
2023-09-25 17:08:10 -04:00

377 lines
12 KiB
Go

package cmd
import (
"fmt"
"os"
"strings"
"alda.io/client/help"
log "alda.io/client/logging"
"alda.io/client/system"
"github.com/spf13/cobra"
)
var verbosity int
// There are certain activities that the Alda CLI performs in the background,
// like sending telemetry and filling the player pool.
//
// We want to avoid prematurely exiting before these activities have completed.
//
// Each activity has a "done" channel, on which a single `true` value is placed
// when the activity completes.
type backgroundActivity struct {
description string
done chan bool
}
// A list of channels, each of which represents an activity happening in the
// background that we want to make sure that we wait for to complete before we
// exit.
//
// Completion of each activity is signaled by placing a single `true` value on
// the channel.
var backgroundActivities []backgroundActivity
func startBackgroundActivity(description string, thunk func()) {
done := make(chan bool)
activity := backgroundActivity{description: description, done: done}
backgroundActivities = append(backgroundActivities, activity)
go func() {
thunk()
done <- true
}()
}
func AwaitBackgroundActivities() {
for _, activity := range backgroundActivities {
log.Debug().
Str("activity", activity.description).
Msg("Waiting for background activity to complete.")
<-activity.done
}
}
// We want to ensure that we fill the player pool in a variety of situations,
// including `alda`, `alda --help`, `alda some-nonexistent-command`, etc.
// Unfortunately, Cobra doesn't give you an easy way to consistently do that.
// PersistentPreRunE _should_ do the trick, but it doesn't run in exceptional
// scenarios like where the user is using flags incorrectly or the user is
// requesting --help.
//
// As a result, we have to hook into "UsageFunc", the function that gets run
// when Cobra prints usage information. But it's possible that both the
// PersistentPreRunE _and_ the UsageFunc will be run, in cases like `alda` being
// run without arguments.
//
// To ensure that we don't double-fill the pool in those scenarios, we use this
// boolean to keep track of whether or not we're already doing it.
var fillingPlayerPool = false
// Fills the player pool, unless we're already doing it.
func fillPlayerPool() {
if !fillingPlayerPool {
startBackgroundActivity("fill the player pool", func() {
if err := system.FillPlayerPool(); err != nil {
log.Warn().Err(err).Msg("Failed to fill player pool.")
}
})
}
fillingPlayerPool = true
}
func sendTelemetry(command string) {
// We don't send telemetry if the user runs `alda` without a subcommand,
// because that's effectively the same thing as running `alda --help`, and we
// don't send telemetry when the user is requesting `--help` either.
if command == "alda" {
return
}
// We will not record telemetry if the user has disabled it by running `alda
// telemetry --disable`.
status, err := readTelemetryStatus()
// If the telemetry status file contains unexpected content, or if we couldn't
// read the file for some reason, the only reasonable thing to do is not to
// record telemetry.
if err != nil {
log.Warn().
Err(err).
Msg("Couldn't determine whether telemetry is enabled.")
return
}
// Don't record telemetry if the user has opted out.
if status == TelemetryDisabled {
return
}
startBackgroundActivity("send telemetry", func() {
if err := sendTelemetryRequest(command); err != nil {
log.Debug().Err(err).Msg("Failed to send telemetry.")
}
})
}
func cleanUpRenamedExecutables() {
startBackgroundActivity("clean up renamed executables", func() {
renamedExecutables, err := system.FindRenamedExecutables()
if err != nil {
log.Warn().Err(err).Msg("Failed to search for renamed executables.")
return
}
for _, filepath := range renamedExecutables {
if err := os.Remove(filepath); err != nil {
log.Warn().
Err(err).
Str("filepath", filepath).
Msg("Failed to delete renamed executable")
} else {
log.Debug().
Str("filepath", filepath).
Msg("Deleted renamed executable.")
}
}
})
}
func init() {
// Inspired by the approach here:
// https://github.com/spf13/cobra/issues/914#issuecomment-548411337
rootCmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error {
return &help.UsageError{Cmd: cmd, Err: err}
})
// Cobra doesn't run my PersistentPreRunE function when there is a usage error
// or `--help` is requested. So, we have to wrap the default usage function
// here with the behavior that we want.
defaultUsageFunc := rootCmd.UsageFunc()
rootCmd.SetUsageFunc(func(cmd *cobra.Command) error {
// handleVerbosity returns a usage error if the supplied verbosity level is
// invalid. We're choosing to ignore it here because the UsageFunc is called
// all over the place, e.g. as a side effect of constructing the usage
// string (see UsageString() in Cobra). In light of this, it feels dangerous
// to override the default UsageFunc with a function that might return an
// error for a reason unrelated to printing usage information.
//
// When an invalid verbosity level is supplied, we fallback to the default
// log level, 1 (warn).
_ = handleVerbosity(cmd)
informUserOfTelemetryIfNeeded()
fillPlayerPool()
return defaultUsageFunc(cmd)
})
// In some cases, the UsageFunc runs. In some cases, the HelpFunc runs,
// followed by the UsageFunc. I've even seen the UsageFunc run twice for some
// weird reason.
//
// The only reason we need a custom HelpFunc is so that in the case of
// informing the user of telemetry on the very first run of Alda, we can make
// sure that we print the notice about telemetry first, before the help text
// (instead of before the usage text, which is further down).
//
// Note that `informUserOfTelemetryIfNeeded` is idempotent. After informing
// the user of telemetry, it writes a `telemetry-status` file which makes it
// so that if we run `informUserOfTelemetryIfNeeded` again, it won't do
// anything because we already informed the user of telemetry.
defaultHelpFunc := rootCmd.HelpFunc()
rootCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
// See the line that looks like this above in SetUsageFunc(...)
_ = handleVerbosity(cmd)
informUserOfTelemetryIfNeeded()
defaultHelpFunc(cmd, args)
})
// This is almost identical to Cobra's default usage template found in the
// source code. I just removed the `{{if .Runnable}}{{.UseLine}}{{end}}` part,
// i.e. the part that suggests that running `alda` with no subcommand is a way
// to use Alda.
//
// Usually, Cobra gets this right, but in this case, because I implemented a
// `RunE` for the root command, Cobra (reasonably) thinks that it should tell
// the user that both `alda` and `alda [command]` are ways to run Alda, when
// in fact, you can't really do anything without a subcommand.
//
// Update: I also had to add an {{else}} branch under Usage: because it was
// just showing up as Usage: with no content in most cases. I'm not sure if
// this is my fault or Cobra's, but I fixed it in the template.
rootCmd.SetUsageTemplate(`Usage:{{if .HasAvailableSubCommands}}
{{.CommandPath}} [command]{{else}}
{{.CommandPath}}{{end}}{{if gt (len .Aliases) 0}}
Aliases:
{{.NameAndAliases}}{{end}}{{if .HasExample}}
Examples:
{{.Example}}{{end}}{{if .HasAvailableSubCommands}}
Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
Flags:
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
Global Flags:
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}
Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
{{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}
Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}
`)
rootCmd.PersistentFlags().IntVarP(
&verbosity, "verbosity", "v", 1, "verbosity level (0-3)",
)
for _, cmd := range []*cobra.Command{
doctorCmd,
exportCmd,
formatCmd,
importCmd,
instrumentsCmd,
parseCmd,
playCmd,
psCmd,
replCmd,
shutdownCmd,
stopCmd,
telemetryCmd,
updateCmd,
versionCmd,
} {
rootCmd.AddCommand(cmd)
}
}
func handleVerbosity(cmd *cobra.Command) error {
switch verbosity {
case 0:
log.SetGlobalLevel("error")
case 1:
log.SetGlobalLevel("warn")
case 2:
log.SetGlobalLevel("info")
case 3:
log.SetGlobalLevel("debug")
default:
log.SetGlobalLevel("warn")
return &help.UsageError{
Cmd: cmd,
Err: fmt.Errorf(
"invalid verbosity level (%d). Valid levels are 0-3",
verbosity,
),
}
}
return nil
}
var rootCmd = &cobra.Command{
Use: "alda",
Short: "alda: a text-based language for music composition",
Long: `alda: a text-based language for music composition
Website: https://alda.io
GitHub: https://github.com/alda-lang/alda
Slack: http://slack.alda.io`,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if err := handleVerbosity(cmd); err != nil {
return err
}
cleanUpRenamedExecutables()
informUserOfTelemetryIfNeeded()
sendTelemetry(cmd.Name())
// Unless the command is one of the exceptions below, Alda will preemptively
// spawn player processes in the background, up to a desired amount. This
// helps to ensure that the application will feel fast, because each time
// you need a player process, there will probably already be one available.
//
// Exceptions:
// * `alda ps` is designed to be run repeatedly, e.g.
// `watch -n 0.25 alda ps`, in order to provide a live-updating view of
// current Alda processes.
//
// * `alda shutdown` shuts down a player process (or all of them, if no
// player ID or port is specified). It's probably fair to assume that if
// someone is running `alda shutdown`, they don't want additional player
// processes to be spawned.
//
// * `alda doctor` spawns its own processes as part of the checks that it
// does, and it simplifies our CI setup if we only spawn those explicit
// ones without also spawning some implicit ones here.
switch cmd.Name() {
case "ps", "shutdown", "doctor":
// Don't fill the player pool.
default:
fillPlayerPool()
}
return nil
},
// I think this is equivalent to the default behavior that Cobra gives you
// when you run the root command with no subcommand; it just prints the help
// text.
//
// Why am I doing it explicitly like this? Because if I don't have a RunE
// function, Cobra won't run my PersistentPreRunE function.
//
// ...And because I have a RunE now, I had to also customize the UsageTemplate
// (see init()) to omit the part that suggests that running `alda` without a
// subcommand is a way to use Alda. Hacks upon hacks.
RunE: func(cmd *cobra.Command, args []string) error {
cmd.Help()
return nil
},
SilenceErrors: true,
SilenceUsage: true,
}
// Execute parses command-line arguments and runs the Alda command-line client.
func Execute() error {
err := rootCmd.Execute()
// Cobra helpfully gives us cmd.SetFlagErrorFunc to allow us to recognize
// usage errors due to incorrect/unrecognized flags so that we can treat them
// specially,, but AFAICT, it doesn't give you any way to do the same thing
// for usage errors due to unrecognized command names. (╯°□°)╯︵ ┻━┻
//
// To work around that, we assume that no other types of errors in the
// application will begin with the string "unknown command" and do the special
// handling here.
//
// Worth noting: Cobra seems to be failing to set the global `verbosity` flag
// value in this scenario, which means in a case like `alda -v3 bogus-cmd`,
// the `-v3` part is ignored by Cobra and `rootCmd.Execute()` just returns the
// "unknown command" error. This is not ideal, but it's sort of an exceptional
// scenario, and the default log level of 1 (warn) is reasonable; the
// important part is that the user can see that they've entered an unknown
// command.
if err != nil && strings.HasPrefix(err.Error(), "unknown command") {
return &help.UsageError{Cmd: rootCmd, Err: err}
}
return err
}