mirror of
https://github.com/alda-lang/alda.git
synced 2026-02-27 18:24:13 +01:00
This is safe to do because that page simply redirects to another page whose URL begins with https://.
377 lines
12 KiB
Go
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
|
|
}
|