Files
seaweedfs/weed/shell/shell_liner.go
T
Chris LuandDevin 6a34d82f05 shell: retry filer discovery until master knows a filer (#11989)
* shell: discover the filer on first use instead of at startup

The startup retry loop made every weed shell invocation wait out the
connect timeout on clusters that have no filer, even for commands that
only talk to the master. Discovery now runs once, on the first command
that actually needs a filer, keeping the retry-until-registered fix for
the fresh-cluster race without stalling master-only commands.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* shell: stop filer discovery retries on non-transient errors

A master that keeps answering ListClusterNodes with a permanent error
(permission denied, unimplemented) cannot gain a filer by retrying, so
discovery returns instead of polling until the deadline.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* shell: explain an empty filer list when discovery times out

* shell: retry filer discovery while the address is still empty

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-23 22:52:51 -07:00

271 lines
6.0 KiB
Go

package shell
import (
"bufio"
"context"
"fmt"
"io"
"os"
"path"
"slices"
"strings"
"time"
"github.com/seaweedfs/seaweedfs/weed/util"
"github.com/seaweedfs/seaweedfs/weed/util/grace"
"github.com/peterh/liner"
flag "github.com/seaweedfs/seaweedfs/weed/util/fla9"
"golang.org/x/term"
)
var historyPath = path.Join(os.TempDir(), "weed-shell")
// Piped stdin returns the last command failure; an interactive session shows
// the error to the operator and keeps going.
func RunShell(options ShellOptions) error {
slices.SortFunc(Commands, func(a, b command) int {
return strings.Compare(a.Name(), b.Name())
})
if !options.Debug {
flag.Set("alsologtostderr", "false")
flag.Set("logtostderr", "false")
}
interactive := liner.TerminalSupported() && term.IsTerminal(int(os.Stdin.Fd())) && term.IsTerminal(int(os.Stdout.Fd()))
var line *liner.State
if interactive {
line = liner.NewLiner()
defer line.Close()
grace.OnInterrupt(func() {
line.Close()
})
line.SetCtrlCAborts(true)
line.SetTabCompletionStyle(liner.TabPrints)
setCompletionHandler(line)
loadHistory(line)
defer saveHistory(line)
}
commandEnv := NewCommandEnv(&options)
connectTimeout := options.ConnectTimeout
if connectTimeout <= 0 {
connectTimeout = 30 * time.Second
}
ctx := context.Background()
connectCtx, cancel := context.WithTimeout(ctx, connectTimeout)
defer cancel()
reconnectCtx, cancelReconnect := context.WithCancel(ctx)
defer cancelReconnect()
go commandEnv.MasterClient.KeepConnectedToMaster(reconnectCtx)
if commandEnv.MasterClient.GetMaster(connectCtx) == "" {
err := fmt.Errorf("cannot connect to master %s within %s", *options.Masters, connectTimeout)
fmt.Fprintln(os.Stderr, err)
return err
}
if interactive {
for {
cmd, err := line.Prompt("> ")
if err != nil {
if err != io.EOF {
fmt.Fprintf(os.Stderr, "%v\n", err)
}
return nil
}
if strings.TrimSpace(cmd) != "" {
line.AppendHistory(cmd)
}
for _, c := range util.StringSplit(cmd, ";") {
if exit, _ := processEachCmd(c, commandEnv); exit {
return nil
}
}
}
} else {
var lastErr error
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
cmd := scanner.Text()
for _, c := range util.StringSplit(cmd, ";") {
exit, err := processEachCmd(c, commandEnv)
if err != nil {
lastErr = err
}
if exit {
return lastErr
}
}
}
if err := scanner.Err(); err != nil {
fmt.Fprintf(os.Stderr, "error reading stdin: %v\n", err)
lastErr = err
}
return lastErr
}
}
func processEachCmd(cmd string, commandEnv *CommandEnv) (exit bool, cmdErr error) {
cmds := splitCommandLine(cmd)
if len(cmds) == 0 {
return false, nil
} else {
args := cmds[1:]
cmd := cmds[0]
if cmd == "help" || cmd == "?" {
printHelp(cmds)
} else if cmd == "exit" || cmd == "quit" {
return true, nil
} else {
foundCommand := false
for _, c := range Commands {
if c.Name() == cmd || c.Name() == "fs."+cmd {
// noLock says "this invocation changes nothing", which is a
// property of the invocation and not of the session. A
// command that set it for a dry run would otherwise leave
// every later command in the session unlocked, so a real
// mutation right after a simulation would skip its lock.
commandEnv.SetNoLock(false)
if err := c.Do(args, commandEnv, os.Stdout); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
cmdErr = err
}
foundCommand = true
}
}
if !foundCommand {
fmt.Fprintf(os.Stderr, "unknown command: %v\n", cmd)
cmdErr = fmt.Errorf("unknown command: %v", cmd)
}
}
}
return false, cmdErr
}
func splitCommandLine(line string) []string {
tokens, _ := parseShellInput(line, true)
return tokens
}
func parseShellInput(line string, split bool) (args []string, unbalanced bool) {
var current strings.Builder
inDoubleQuotes := false
inSingleQuotes := false
escaped := false
for i := 0; i < len(line); i++ {
c := line[i]
if escaped {
current.WriteByte(c)
escaped = false
continue
}
if c == '\\' && !inSingleQuotes {
escaped = true
continue
}
if c == '"' && !inSingleQuotes {
inDoubleQuotes = !inDoubleQuotes
continue
}
if c == '\'' && !inDoubleQuotes {
inSingleQuotes = !inSingleQuotes
continue
}
if split && (c == ' ' || c == '\t' || c == '\n' || c == '\r') && !inDoubleQuotes && !inSingleQuotes {
if current.Len() > 0 {
args = append(args, current.String())
current.Reset()
}
continue
}
current.WriteByte(c)
}
if current.Len() > 0 {
args = append(args, current.String())
}
return args, inDoubleQuotes || inSingleQuotes || escaped
}
func printGenericHelp() {
msg :=
`Type: "help <command>" for help on <command>. Most commands support "<command> -h" also for options.
`
fmt.Print(msg)
for _, c := range Commands {
if c.HasTag(Hidden) {
continue
}
helpTexts := strings.SplitN(c.Help(), "\n", 2)
fmt.Printf(" %-30s\t# %s \n", c.Name(), helpTexts[0])
}
}
func printHelp(cmds []string) {
args := cmds[1:]
if len(args) == 0 {
printGenericHelp()
} else if len(args) > 1 {
fmt.Println()
} else {
cmd := strings.ToLower(args[0])
for _, c := range Commands {
if strings.ToLower(c.Name()) == cmd {
fmt.Printf(" %s\t# %s\n", c.Name(), c.Help())
fmt.Printf("use \"%s -h\" for more details\n", c.Name())
}
}
}
}
func setCompletionHandler(line *liner.State) {
line.SetCompleter(func(line string) (c []string) {
for _, i := range Commands {
if strings.HasPrefix(i.Name(), strings.ToLower(line)) {
c = append(c, i.Name())
}
}
return
})
}
func loadHistory(line *liner.State) {
if f, err := os.Open(historyPath); err == nil {
line.ReadHistory(f)
f.Close()
}
}
func saveHistory(line *liner.State) {
if f, err := os.Create(historyPath); err != nil {
fmt.Fprintf(os.Stderr, "Error creating history file: %v\n", err)
} else {
if _, err = line.WriteHistory(f); err != nil {
fmt.Fprintf(os.Stderr, "Error writing history file: %v\n", err)
}
f.Close()
}
}