Files
Chris LuandCarlos Leyva 9089a546fb shell: exit non-zero when a piped command fails (#11117)
* shell: non-interactive mode exits non-zero when a command fails

A failed command in a piped weed shell run printed 'error: ...' but the process
still exited 0, so a CronJob wrapping e.g.

  echo 's3.lifecycle.run-shard -shards 0-15' | weed shell -master=...

reported green while the run aborted partway (shards N+1..15 unwalked). An
unknown command likewise exited 0.

RunShell now returns the last command failure from the non-interactive stdin
path (unknown commands included), and the shell command exits 2 on it.
Interactive sessions are unchanged: errors are shown to the operator and the
session continues, exiting 0 as before.

* shell: route the piped-failure exit through main's shutdown path

Review follow-up: os.Exit(2) inside the shell command skipped main's shutdown
work. The command now records the status (SetCommandExitStatus) and returns
normally; main applies it via setExitStatus before exit(). exit() itself now
flushes sentry before os.Exit -- main's deferred sentry.Flush never ran on this
path (os.Exit skips defers), so the existing 'flush buffered events before the
program terminates' intent only worked for the autocomplete early-return.
Exit status 2 on a failed piped run is preserved (verified: piped success
exits 0, piped failing command exits 2).

* shell: test the registered-command failure path

Review follow-up: the error-propagation test only covered unknown commands.
A fake registered command now drives processEachCmd's real dispatch path:
a failing Do surfaces its exact error (errors.Is) and a succeeding one
returns nil. The non-interactive exit status itself is main-level plumbing,
verified end to end against the reproduction (piped failure exits 2).

* shell: trim the comments added with the exit status

Keep the non-obvious why -- why a piped run has to fail its wrapper, why the
status is recorded instead of os.Exit'ed -- and drop the narration.

Claude-Session: https://claude.ai/code/session_018DWwctzD4T2DmnczPRM47t

* shell: fail a piped run with the status weed already uses for that

weed.go spends 1 on a command that failed and 2 on a usage or syntax error, and
runShell returns true precisely so the usage dump is skipped. Exiting 2 there
told a wrapper the command line was wrong.

Claude-Session: https://claude.ai/code/session_018DWwctzD4T2DmnczPRM47t

---------

Co-authored-by: Carlos Leyva <carlos.leyva@idener.es>
2026-09-02 22:12:51 -07:00

209 lines
4.4 KiB
Go

package main
import (
"embed"
"fmt"
"io"
"io/fs"
"os"
"strings"
"sync"
"text/template"
"time"
"unicode"
"unicode/utf8"
weed_server "github.com/seaweedfs/seaweedfs/weed/server"
"github.com/seaweedfs/seaweedfs/weed/util"
flag "github.com/seaweedfs/seaweedfs/weed/util/fla9"
"github.com/getsentry/sentry-go"
"github.com/seaweedfs/seaweedfs/weed/command"
"github.com/seaweedfs/seaweedfs/weed/glog"
util_http "github.com/seaweedfs/seaweedfs/weed/util/http"
)
var IsDebug *bool
var commands = command.Commands
var exitStatus = 0
var exitMu sync.Mutex
func setExitStatus(n int) {
exitMu.Lock()
if exitStatus < n {
exitStatus = n
}
exitMu.Unlock()
}
//go:embed static
var static embed.FS
func init() {
weed_server.StaticFS, _ = fs.Sub(static, "static")
flag.Var(&util.ConfigurationFileDirectory, "config_dir", "directory with toml configuration files")
}
func main() {
glog.MaxSize = 1024 * 1024 * 10
glog.MaxFileCount = 5
flag.Usage = usage
err := sentry.Init(sentry.ClientOptions{
SampleRate: 0.1,
EnableTracing: true,
TracesSampleRate: 0.1,
})
if err != nil {
fmt.Fprintf(os.Stderr, "sentry.Init: %v", err)
}
// Flush buffered events before the program terminates.
// Set the timeout to the maximum duration the program can afford to wait.
defer sentry.Flush(2 * time.Second)
if command.AutocompleteMain(commands) {
return
}
flag.Parse()
args := flag.Args()
if len(args) < 1 {
usage()
}
if args[0] == "help" {
help(args[1:])
for _, cmd := range commands {
if len(args) >= 2 && cmd.Name() == args[1] && cmd.Run != nil {
fmt.Fprintf(os.Stderr, "Default Parameters:\n")
cmd.Flag.PrintDefaults()
}
}
return
}
if args[0] != command.GetFuseCommandName() {
util_http.InitGlobalHttpClient()
}
for _, cmd := range commands {
if cmd.Name() == args[0] && cmd.Run != nil {
cmd.Flag.Usage = func() { cmd.Usage() }
cmd.Flag.Parse(args[1:])
args = cmd.Flag.Args()
IsDebug = cmd.IsDebug
if !cmd.Run(cmd, args) {
fmt.Fprintf(os.Stderr, "\n")
cmd.Flag.Usage()
fmt.Fprintf(os.Stderr, "Default Parameters:\n")
cmd.Flag.PrintDefaults()
// Command execution failed - general error
setExitStatus(1)
}
// A command can also record a failure without returning false.
setExitStatus(command.CommandExitStatus())
exit()
return
}
}
// Unknown command - syntax error
fmt.Fprintf(os.Stderr, "weed: unknown subcommand %q\nRun 'weed help' for usage.\n", args[0])
setExitStatus(2)
exit()
}
var usageTemplate = `
SeaweedFS: store billions of files and serve them fast!
Usage:
weed command [arguments]
The commands are:
{{range .}}{{if .Runnable}}
{{.Name | printf "%-11s"}} {{.Short}}{{end}}{{end}}
Use "weed help [command]" for more information about a command.
`
var helpTemplate = `{{if .Runnable}}Usage: weed {{.UsageLine}}
{{end}}
{{.Long}}
`
// tmpl executes the given template text on data, writing the result to w.
func tmpl(w io.Writer, text string, data interface{}) {
t := template.New("top")
t.Funcs(template.FuncMap{"trim": strings.TrimSpace, "capitalize": capitalize})
template.Must(t.Parse(text))
if err := t.Execute(w, data); err != nil {
panic(err)
}
}
func capitalize(s string) string {
if s == "" {
return s
}
r, n := utf8.DecodeRuneInString(s)
return string(unicode.ToTitle(r)) + s[n:]
}
func printUsage(w io.Writer) {
tmpl(w, usageTemplate, commands)
}
func usage() {
printUsage(os.Stderr)
fmt.Fprintf(os.Stderr, "For Logging, use \"weed [logging_options] [command]\". The logging options are:\n")
flag.PrintDefaults()
// Invalid command line usage - syntax error
setExitStatus(2)
exit()
}
// help implements the 'help' command.
func help(args []string) {
if len(args) == 0 {
printUsage(os.Stdout)
// Success - help displayed correctly
return
}
if len(args) != 1 {
fmt.Fprintf(os.Stderr, "usage: weed help command\n\nToo many arguments given.\n")
// Invalid help usage - syntax error
setExitStatus(2)
exit()
}
arg := args[0]
for _, cmd := range commands {
if cmd.Name() == arg {
tmpl(os.Stdout, helpTemplate, cmd)
// Success - help for specific command displayed correctly
return
}
}
fmt.Fprintf(os.Stderr, "Unknown help topic %#q. Run 'weed help'.\n", arg)
// Unknown help topic - syntax error
setExitStatus(2)
exit()
}
var atexitFuncs []func()
func exit() {
for _, f := range atexitFuncs {
f()
}
// os.Exit below skips main's deferred flush.
sentry.Flush(2 * time.Second)
os.Exit(exitStatus)
}