cmd/syncthing: Add cli as a subcommand (fixes #6566, fixes #4719) (#7364)

* cmd/syncthing: Add cli as a subcommand (fixes #6566, fixes #4719)

* Hijack help

* Add comment

* Revert go.mod/go.sum
This commit is contained in:
Audrius Butkevicius 2021-02-15 17:50:53 +00:00 committed by GitHub
parent b2c9e7b07b
commit fb078068b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 107 additions and 88 deletions

View File

@ -4,7 +4,7 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file, // License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/. // You can obtain one at https://mozilla.org/MPL/2.0/.
package main package cli
import ( import (
"bytes" "bytes"

View File

@ -4,7 +4,7 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file, // License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/. // You can obtain one at https://mozilla.org/MPL/2.0/.
package main package cli
import ( import (
"errors" "errors"

View File

@ -4,13 +4,12 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file, // License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/. // You can obtain one at https://mozilla.org/MPL/2.0/.
package main package cli
import ( import (
"bufio" "bufio"
"crypto/tls" "crypto/tls"
"encoding/json" "encoding/json"
"flag"
"log" "log"
"os" "os"
"reflect" "reflect"
@ -19,67 +18,56 @@ import (
"github.com/flynn-archive/go-shlex" "github.com/flynn-archive/go-shlex"
"github.com/mattn/go-isatty" "github.com/mattn/go-isatty"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/locations" "github.com/syncthing/syncthing/lib/locations"
"github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/svcutil"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
func main() { type CLI struct {
// This is somewhat a hack around a chicken and egg problem. GUIAddress string `name:"gui-address" placeholder:"URL" help:"Override GUI address (e.g. \"http://192.0.2.42:8443\")"`
// We need to set the home directory and potentially other flags to know where the syncthing instance is running GUIAPIKey string `name:"gui-apikey" placeholder:"API-KEY" help:"Override GUI API key"`
// in order to get it's config ... which we then use to construct the actual CLI ... at which point it's too late HomeDir string `name:"home" placeholder:"PATH" help:"Set configuration and data directory"`
// to add flags there... ConfDir string `name:"conf" placeholder:"PATH" help:"Set configuration directory (config and keys)"`
homeBaseDir := locations.GetBaseDir(locations.ConfigBaseDir) Args []string `arg:"" optional:""`
guiCfg := config.GUIConfiguration{} }
flags := flag.NewFlagSet("", flag.ContinueOnError) func (c *CLI) Run() error {
flags.StringVar(&guiCfg.RawAddress, "gui-address", guiCfg.RawAddress, "Override GUI address (e.g. \"http://192.0.2.42:8443\")") // Not set as default above because the strings can be really long.
flags.StringVar(&guiCfg.APIKey, "gui-apikey", guiCfg.APIKey, "Override GUI API key") var err error
flags.StringVar(&homeBaseDir, "home", homeBaseDir, "Set configuration directory") homeSet := c.HomeDir != ""
confSet := c.ConfDir != ""
// Implement the same flags at the lower CLI, with the same default values (pre-parse), but do nothing with them. switch {
// This is so that we could reuse os.Args case homeSet && confSet:
fakeFlags := []cli.Flag{ err = errors.New("-home must not be used together with -conf")
cli.StringFlag{ case homeSet:
Name: "gui-address", err = locations.SetBaseDir(locations.ConfigBaseDir, c.HomeDir)
Value: guiCfg.RawAddress, case confSet:
Usage: "Override GUI address (e.g. \"http://192.0.2.42:8443\")", err = locations.SetBaseDir(locations.ConfigBaseDir, c.ConfDir)
}, }
cli.StringFlag{ if err != nil {
Name: "gui-apikey", log.Println("Command line options:", err)
Value: guiCfg.APIKey, os.Exit(svcutil.ExitError.AsInt())
Usage: "Override GUI API key", }
}, guiCfg := config.GUIConfiguration{
cli.StringFlag{ RawAddress: c.GUIAddress,
Name: "home", APIKey: c.GUIAPIKey,
Value: homeBaseDir,
Usage: "Set configuration directory",
},
} }
// Do not print usage of these flags, and ignore errors as this can't understand plenty of things
flags.Usage = func() {}
_ = flags.Parse(os.Args[1:])
// Now if the API key and address is not provided (we are not connecting to a remote instance), // Now if the API key and address is not provided (we are not connecting to a remote instance),
// try to rip it out of the config. // try to rip it out of the config.
if guiCfg.RawAddress == "" && guiCfg.APIKey == "" { if guiCfg.RawAddress == "" && guiCfg.APIKey == "" {
// Update the base directory
err := locations.SetBaseDir(locations.ConfigBaseDir, homeBaseDir)
if err != nil {
log.Fatal(errors.Wrap(err, "setting home"))
}
// Load the certs and get the ID // Load the certs and get the ID
cert, err := tls.LoadX509KeyPair( cert, err := tls.LoadX509KeyPair(
locations.Get(locations.CertFile), locations.Get(locations.CertFile),
locations.Get(locations.KeyFile), locations.Get(locations.KeyFile),
) )
if err != nil { if err != nil {
log.Fatal(errors.Wrap(err, "reading device ID")) return errors.Wrap(err, "reading device ID")
} }
myID := protocol.NewDeviceID(cert.Certificate[0]) myID := protocol.NewDeviceID(cert.Certificate[0])
@ -87,20 +75,20 @@ func main() {
// Load the config // Load the config
cfg, _, err := config.Load(locations.Get(locations.ConfigFile), myID, events.NoopLogger) cfg, _, err := config.Load(locations.Get(locations.ConfigFile), myID, events.NoopLogger)
if err != nil { if err != nil {
log.Fatalln(errors.Wrap(err, "loading config")) return errors.Wrap(err, "loading config")
} }
guiCfg = cfg.GUI() guiCfg = cfg.GUI()
} else if guiCfg.Address() == "" || guiCfg.APIKey == "" { } else if guiCfg.Address() == "" || guiCfg.APIKey == "" {
log.Fatalln("Both -gui-address and -gui-apikey should be specified") return errors.New("Both --gui-address and --gui-apikey should be specified")
} }
if guiCfg.Address() == "" { if guiCfg.Address() == "" {
log.Fatalln("Could not find GUI Address") return errors.New("Could not find GUI Address")
} }
if guiCfg.APIKey == "" { if guiCfg.APIKey == "" {
log.Fatalln("Could not find GUI API key") return errors.New("Could not find GUI API key")
} }
client := getClient(guiCfg) client := getClient(guiCfg)
@ -108,7 +96,7 @@ func main() {
cfg, err := getConfig(client) cfg, err := getConfig(client)
original := cfg.Copy() original := cfg.Copy()
if err != nil { if err != nil {
log.Fatalln(errors.Wrap(err, "getting config")) return errors.Wrap(err, "getting config")
} }
// Copy the config and set the default flags // Copy the config and set the default flags
@ -118,16 +106,40 @@ func main() {
commands, err := recli.New(recliCfg).Construct(&cfg) commands, err := recli.New(recliCfg).Construct(&cfg)
if err != nil { if err != nil {
log.Fatalln(errors.Wrap(err, "config reflect")) return errors.Wrap(err, "config reflect")
}
// Implement the same flags at the upper CLI, but do nothing with them.
// This is so that the usage text is the same
fakeFlags := []cli.Flag{
cli.StringFlag{
Name: "gui-address",
Value: "URL",
Usage: "Override GUI address (e.g. \"http://192.0.2.42:8443\")",
},
cli.StringFlag{
Name: "gui-apikey",
Value: "API-KEY",
Usage: "Override GUI API key",
},
cli.StringFlag{
Name: "home",
Value: "PATH",
Usage: "Set configuration and data directory",
},
cli.StringFlag{
Name: "conf",
Value: "PATH",
Usage: "Set configuration directory (config and keys)",
},
} }
// Construct the actual CLI // Construct the actual CLI
app := cli.NewApp() app := cli.NewApp()
app.Name = "stcli" app.Name = "syncthing cli"
app.HelpName = app.Name app.HelpName = app.Name
app.Author = "The Syncthing Authors" app.Author = "The Syncthing Authors"
app.Usage = "Syncthing command line interface" app.Usage = "Syncthing command line interface"
app.Version = build.Version
app.Flags = fakeFlags app.Flags = fakeFlags
app.Metadata = map[string]interface{}{ app.Metadata = map[string]interface{}{
"client": client, "client": client,
@ -144,6 +156,9 @@ func main() {
errorsCommand, errorsCommand,
} }
// It expects to be give os.Args which has argv[0] set to executable name, so fake it.
c.Args = append([]string{"cli"}, c.Args...)
tty := isatty.IsTerminal(os.Stdin.Fd()) || isatty.IsCygwinTerminal(os.Stdin.Fd()) tty := isatty.IsTerminal(os.Stdin.Fd()) || isatty.IsCygwinTerminal(os.Stdin.Fd())
if !tty { if !tty {
// Not a TTY, consume from stdin // Not a TTY, consume from stdin
@ -151,42 +166,43 @@ func main() {
for scanner.Scan() { for scanner.Scan() {
input, err := shlex.Split(scanner.Text()) input, err := shlex.Split(scanner.Text())
if err != nil { if err != nil {
log.Fatalln(errors.Wrap(err, "parsing input")) return errors.Wrap(err, "parsing input")
} }
if len(input) == 0 { if len(input) == 0 {
continue continue
} }
err = app.Run(append(os.Args, input...)) err = app.Run(append(c.Args, input...))
if err != nil { if err != nil {
log.Fatalln(err) return err
} }
} }
err = scanner.Err() err = scanner.Err()
if err != nil { if err != nil {
log.Fatalln(err) return err
} }
} else { } else {
err = app.Run(os.Args) err = app.Run(c.Args)
if err != nil { if err != nil {
log.Fatalln(err) return err
} }
} }
if !reflect.DeepEqual(cfg, original) { if !reflect.DeepEqual(cfg, original) {
body, err := json.MarshalIndent(cfg, "", " ") body, err := json.MarshalIndent(cfg, "", " ")
if err != nil { if err != nil {
log.Fatalln(err) return err
} }
resp, err := client.Post("system/config", string(body)) resp, err := client.Post("system/config", string(body))
if err != nil { if err != nil {
log.Fatalln(err) return err
} }
if resp.StatusCode != 200 { if resp.StatusCode != 200 {
body, err := responseToBArray(resp) body, err := responseToBArray(resp)
if err != nil { if err != nil {
log.Fatalln(err) return err
} }
log.Fatalln(string(body)) return errors.New(string(body))
} }
} }
return nil
} }

View File

@ -4,7 +4,7 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file, // License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/. // You can obtain one at https://mozilla.org/MPL/2.0/.
package main package cli
import ( import (
"fmt" "fmt"

View File

@ -4,7 +4,7 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file, // License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/. // You can obtain one at https://mozilla.org/MPL/2.0/.
package main package cli
import ( import (
"github.com/urfave/cli" "github.com/urfave/cli"

View File

@ -4,7 +4,7 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file, // License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/. // You can obtain one at https://mozilla.org/MPL/2.0/.
package main package cli
import ( import (
"encoding/json" "encoding/json"

View File

@ -31,6 +31,9 @@ import (
"time" "time"
"github.com/alecthomas/kong" "github.com/alecthomas/kong"
"github.com/thejerf/suture/v4"
"github.com/syncthing/syncthing/cmd/syncthing/cli"
"github.com/syncthing/syncthing/cmd/syncthing/decrypt" "github.com/syncthing/syncthing/cmd/syncthing/decrypt"
"github.com/syncthing/syncthing/lib/build" "github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/config"
@ -48,7 +51,6 @@ import (
"github.com/syncthing/syncthing/lib/upgrade" "github.com/syncthing/syncthing/lib/upgrade"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/thejerf/suture/v4"
) )
const ( const (
@ -127,11 +129,12 @@ var (
errTooEarlyUpgrade = fmt.Errorf("last upgrade happened less than %v ago, skipping", upgradeRetryInterval) errTooEarlyUpgrade = fmt.Errorf("last upgrade happened less than %v ago, skipping", upgradeRetryInterval)
) )
// The cli struct is the main entry point for the command line parser. The // The entrypoint struct is the main entry point for the command line parser. The
// commands and options here are top level commands to syncthing. // commands and options here are top level commands to syncthing.
var cli struct { var entrypoint struct {
Serve serveOptions `cmd:"" help:"Run Syncthing"` Serve serveOptions `cmd:"" help:"Run Syncthing"`
Decrypt decrypt.CLI `cmd:"" help:"Decrypt or verify an encrypted folder"` Decrypt decrypt.CLI `cmd:"" help:"Decrypt or verify an encrypted folder"`
Cli cli.CLI `cmd:"" help:"Command line interface for Syncthing"`
} }
// serveOptions are the options for the `syncthing serve` command. // serveOptions are the options for the `syncthing serve` command.
@ -227,11 +230,11 @@ func main() {
args = append([]string{"serve"}, convertLegacyArgs(args)...) args = append([]string{"serve"}, convertLegacyArgs(args)...)
} }
cli.Serve.setDefaults() entrypoint.Serve.setDefaults()
// Create a parser with an overridden help function to print our extra // Create a parser with an overridden help function to print our extra
// help info. // help info.
parser, err := kong.New(&cli, kong.Help(extraHelpPrinter)) parser, err := kong.New(&entrypoint, kong.Help(helpHandler))
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -242,7 +245,11 @@ func main() {
parser.FatalIfErrorf(err) parser.FatalIfErrorf(err)
} }
func extraHelpPrinter(options kong.HelpOptions, ctx *kong.Context) error { func helpHandler(options kong.HelpOptions, ctx *kong.Context) error {
// If we're looking for CLI help, pass the arguments down to the CLI library to print it's own help.
if ctx.Command() == "cli" {
return ctx.Run()
}
if err := kong.DefaultHelpPrinter(options, ctx); err != nil { if err := kong.DefaultHelpPrinter(options, ctx); err != nil {
return err return err
} }
@ -282,12 +289,12 @@ func (options serveOptions) Run() error {
case homeSet && dataSet: case homeSet && dataSet:
err = errors.New("-home must not be used together with -conf and -data") err = errors.New("-home must not be used together with -conf and -data")
case homeSet: case homeSet:
if err = setLocation(locations.ConfigBaseDir, options.HomeDir); err == nil { if err = locations.SetBaseDir(locations.ConfigBaseDir, options.HomeDir); err == nil {
err = setLocation(locations.DataBaseDir, options.HomeDir) err = locations.SetBaseDir(locations.DataBaseDir, options.HomeDir)
} }
case dataSet: case dataSet:
if err = setLocation(locations.ConfigBaseDir, options.ConfDir); err == nil { if err = locations.SetBaseDir(locations.ConfigBaseDir, options.ConfDir); err == nil {
err = setLocation(locations.DataBaseDir, options.DataDir) err = locations.SetBaseDir(locations.DataBaseDir, options.DataDir)
} }
} }
if err != nil { if err != nil {
@ -1004,17 +1011,6 @@ func exitCodeForUpgrade(err error) int {
return svcutil.ExitError.AsInt() return svcutil.ExitError.AsInt()
} }
func setLocation(enum locations.BaseDirEnum, loc string) error {
if !filepath.IsAbs(loc) {
var err error
loc, err = filepath.Abs(loc)
if err != nil {
return err
}
}
return locations.SetBaseDir(enum, loc)
}
// convertLegacyArgs returns the slice of arguments with single dash long // convertLegacyArgs returns the slice of arguments with single dash long
// flags converted to double dash long flags. // flags converted to double dash long flags.
func convertLegacyArgs(args []string) []string { func convertLegacyArgs(args []string) []string {

View File

@ -68,6 +68,13 @@ func init() {
} }
func SetBaseDir(baseDirName BaseDirEnum, path string) error { func SetBaseDir(baseDirName BaseDirEnum, path string) error {
if !filepath.IsAbs(path) {
var err error
path, err = filepath.Abs(path)
if err != nil {
return err
}
}
_, ok := baseDirs[baseDirName] _, ok := baseDirs[baseDirName]
if !ok { if !ok {
return fmt.Errorf("unknown base dir: %s", baseDirName) return fmt.Errorf("unknown base dir: %s", baseDirName)