package main // The trap builtin. A trapped signal is only noted when it arrives; its // command runs in the shell at the next safe point: after the running // command has finished (like in sh, a shell waiting for a foreground command // handles the signal afterwards). import ( "fmt" "io" "os" "os/signal" "sort" "strconv" "strings" "sync" "syscall" ) // trapSignals are the signals that can be trapped, besides EXIT. var trapSignals = map[string]syscall.Signal{ "HUP": syscall.SIGHUP, "INT": syscall.SIGINT, "QUIT": syscall.SIGQUIT, "TERM": syscall.SIGTERM, "USR1": syscall.SIGUSR1, "USR2": syscall.SIGUSR2, "ALRM": syscall.SIGALRM, "WINCH": syscall.SIGWINCH, } // fatalSignals end a non-interactive shell; with an EXIT trap the shell // catches them to run it first. var fatalSignals = []string{"HUP", "INT", "TERM"} var ( // traps maps EXIT and signal names to their commands ("" = ignore). traps = map[string]string{} // trapChans are the channels of the signals the shell catches. trapChans = map[string]chan os.Signal{} pendingMu sync.Mutex pendingSignals []string inTrap bool // interruptChan swallows SIGINT in an interactive shell, so that Ctrl+C // only stops the running command. interruptChan = make(chan os.Signal, 1) ) // signalName returns the canonical name (EXIT, INT, ...) of a signal given // by name (with or without SIG) or number, or "". func signalName(spec string) string { s := strings.ToUpper(strings.TrimPrefix(strings.ToUpper(spec), "SIG")) if s == "EXIT" || s == "0" { return "EXIT" } if _, ok := trapSignals[s]; ok { return s } if n, err := strconv.Atoi(spec); err == nil { for name, sig := range trapSignals { if int(sig) == n { return name } } } return "" } // builtinTrap implements trap [-lp] [[action] signal ...]. func builtinTrap(args []string, stdout io.Writer) error { args = args[1:] switch { case len(args) == 0 || args[0] == "-p": names := args if len(names) > 0 { names = names[1:] } return printTraps(stdout, names) case args[0] == "-l": names := []string{"EXIT"} for name := range trapSignals { names = append(names, name) } sort.Slice(names, func(i, j int) bool { return signalNumber(names[i]) < signalNumber(names[j]) }) for _, name := range names { fmt.Fprintf(stdout, "%2d) %s\n", signalNumber(name), name) } return nil case args[0] == "--": args = args[1:] } action := "-" if len(args) > 1 || len(args) == 1 && signalName(args[0]) == "" { action, args = args[0], args[1:] } if len(args) == 0 { return fmt.Errorf("trap: usage: trap [-lp] [[action] signal ...]") } var err error for _, spec := range args { name := signalName(spec) if name == "" { err = fmt.Errorf("trap: %s: invalid signal specification", sanitize(spec)) continue } setTrap(name, action) } return err } func signalNumber(name string) int { if name == "EXIT" { return 0 } return int(trapSignals[name]) } func printTraps(w io.Writer, only []string) error { var names []string for name := range traps { names = append(names, name) } if len(only) > 0 { names = nil for _, spec := range only { name := signalName(spec) if name == "" { return fmt.Errorf("trap: %s: invalid signal specification", sanitize(spec)) } if _, ok := traps[name]; ok { names = append(names, name) } } } sort.Slice(names, func(i, j int) bool { return signalNumber(names[i]) < signalNumber(names[j]) }) for _, name := range names { fmt.Fprintf(w, "trap -- %s %s\n", quoteAlias(traps[name]), name) } return nil } // setTrap sets the action of a trap: "-" restores the default, "" ignores // the signal, anything else is a command. func setTrap(name, action string) { if action == "-" { delete(traps, name) } else { traps[name] = action } if name == "EXIT" { updateFatalCatch() return } sig := trapSignals[name] stopCatching(name) switch action { case "-": signal.Reset(sig) restoreShellSignal(sig) if _, exit := traps["EXIT"]; exit { updateFatalCatch() } case "": // Ignored signals stay ignored in the programs the shell starts signal.Ignore(sig) default: catch(name) } } // catch makes the shell note the signal, to run its trap later. func catch(name string) { if _, ok := trapChans[name]; ok { return } ch := make(chan os.Signal, 4) trapChans[name] = ch signal.Notify(ch, trapSignals[name]) go func() { for range ch { pendingMu.Lock() pendingSignals = append(pendingSignals, name) pendingMu.Unlock() } }() } func stopCatching(name string) { if ch, ok := trapChans[name]; ok { signal.Stop(ch) close(ch) delete(trapChans, name) } } // restoreShellSignal installs the handling the shell itself needs for sig // again after signal.Reset. func restoreShellSignal(sig syscall.Signal) { if sig == syscall.SIGINT && interactiveShell { signal.Notify(interruptChan, os.Interrupt) } } // updateFatalCatch catches the fatal signals in a non-interactive shell with // an EXIT trap (unless they have traps of their own), so that the EXIT trap // runs before the shell ends. func updateFatalCatch() { if interactiveShell { return } _, exitTrap := traps["EXIT"] for _, name := range fatalSignals { if _, own := traps[name]; own { continue } if exitTrap { catch(name) } else { stopCatching(name) } } } // hasPendingSignal reports whether a caught signal waits for its trap. func hasPendingSignal() bool { pendingMu.Lock() defer pendingMu.Unlock() return len(pendingSignals) > 0 } // runPendingTraps runs the traps of the signals that arrived. A fatal signal // without a trap of its own (caught for the EXIT trap) ends the shell. func runPendingTraps() { if inTrap { return } pendingMu.Lock() names := pendingSignals pendingSignals = nil pendingMu.Unlock() for _, name := range names { if action, ok := traps[name]; ok { if action != "" { runTrap(action) } continue } // Caught only for the EXIT trap: run it and die from the signal runExitTrap() sig := trapSignals[name] signal.Reset(sig) releaseTerminal() syscall.Kill(os.Getpid(), sig) os.Exit(128 + int(sig)) } } // runTrap runs the command of a trap; $? is kept. func runTrap(action string) { saved := lastStatus inTrap = true defer func() { inTrap = false }() tree, err := parse(action) if err == nil { err = tree.exec() } printError(err) lastStatus = saved } // runExitTrap runs the EXIT trap (once) before the shell exits. func runExitTrap() { action, ok := traps["EXIT"] if !ok { return } delete(traps, "EXIT") if action != "" { runTrap(action) } }