cmd/tailscale/cli: add Stdout, Stderr and output through them

So js/wasm can override where those go, without implementing
an *os.File pipe pair, etc.

Updates #3157

Change-Id: I14ba954d9f2349ff15b58796d95ecb1367e8ba3a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
pull/3202/head
Brad Fitzpatrick 3 years ago
parent 2ce5fc7b0a
commit 5df7ac70d6

@ -7,7 +7,6 @@ package cli
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"github.com/peterbourgon/ff/v3/ffcli" "github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale" "tailscale.com/client/tailscale"
@ -33,6 +32,6 @@ func runBugReport(ctx context.Context, args []string) error {
if err != nil { if err != nil {
return err return err
} }
fmt.Println(logMarker) outln(logMarker)
return nil return nil
} }

@ -81,7 +81,7 @@ func runCert(ctx context.Context, args []string) error {
domain := args[0] domain := args[0]
printf := func(format string, a ...interface{}) { printf := func(format string, a ...interface{}) {
fmt.Printf(format, a...) printf(format, a...)
} }
if certArgs.certFile == "-" || certArgs.keyFile == "-" { if certArgs.certFile == "-" || certArgs.keyFile == "-" {
printf = log.Printf printf = log.Printf
@ -143,7 +143,7 @@ func runCert(ctx context.Context, args []string) error {
func writeIfChanged(filename string, contents []byte, mode os.FileMode) (changed bool, err error) { func writeIfChanged(filename string, contents []byte, mode os.FileMode) (changed bool, err error) {
if filename == "-" { if filename == "-" {
os.Stdout.Write(contents) Stdout.Write(contents)
return false, nil return false, nil
} }
if old, err := os.ReadFile(filename); err == nil && bytes.Equal(contents, old) { if old, err := os.ReadFile(filename); err == nil && bytes.Equal(contents, old) {

@ -31,6 +31,22 @@ import (
"tailscale.com/syncs" "tailscale.com/syncs"
) )
var Stderr io.Writer = os.Stderr
var Stdout io.Writer = os.Stdout
func printf(format string, a ...interface{}) {
fmt.Fprintf(Stdout, format, a...)
}
// outln is like fmt.Println in the common case, except when Stdout is
// changed (as in js/wasm).
//
// It's not named println because that looks like the Go built-in
// which goes to stderr and formats slightly differently.
func outln(a ...interface{}) {
fmt.Fprintln(Stdout, a...)
}
// ActLikeCLI reports whether a GUI application should act like the // ActLikeCLI reports whether a GUI application should act like the
// CLI based on os.Args, GOOS, the context the process is running in // CLI based on os.Args, GOOS, the context the process is running in
// (pty, parent PID), etc. // (pty, parent PID), etc.
@ -82,7 +98,9 @@ func newFlagSet(name string) *flag.FlagSet {
if runtime.GOOS == "js" { if runtime.GOOS == "js" {
onError = flag.ContinueOnError onError = flag.ContinueOnError
} }
return flag.NewFlagSet(name, onError) fs := flag.NewFlagSet(name, onError)
fs.SetOutput(Stderr)
return fs
} }
// Run runs the CLI. The args do not include the binary name. // Run runs the CLI. The args do not include the binary name.
@ -94,7 +112,7 @@ func Run(args []string) error {
var warnOnce sync.Once var warnOnce sync.Once
tailscale.SetVersionMismatchHandler(func(clientVer, serverVer string) { tailscale.SetVersionMismatchHandler(func(clientVer, serverVer string) {
warnOnce.Do(func() { warnOnce.Do(func() {
fmt.Fprintf(os.Stderr, "Warning: client version %q != tailscaled server version %q\n", clientVer, serverVer) fmt.Fprintf(Stderr, "Warning: client version %q != tailscaled server version %q\n", clientVer, serverVer)
}) })
}) })

@ -61,7 +61,7 @@ var debugArgs struct {
func writeProfile(dst string, v []byte) error { func writeProfile(dst string, v []byte) error {
if dst == "-" { if dst == "-" {
_, err := os.Stdout.Write(v) _, err := Stdout.Write(v)
return err return err
} }
return os.WriteFile(dst, v, 0600) return os.WriteFile(dst, v, 0600)
@ -83,21 +83,21 @@ func runDebug(ctx context.Context, args []string) error {
} }
if debugArgs.env { if debugArgs.env {
for _, e := range os.Environ() { for _, e := range os.Environ() {
fmt.Println(e) outln(e)
} }
return nil return nil
} }
if debugArgs.localCreds { if debugArgs.localCreds {
port, token, err := safesocket.LocalTCPPortAndToken() port, token, err := safesocket.LocalTCPPortAndToken()
if err == nil { if err == nil {
fmt.Printf("curl -u:%s http://localhost:%d/localapi/v0/status\n", token, port) printf("curl -u:%s http://localhost:%d/localapi/v0/status\n", token, port)
return nil return nil
} }
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
fmt.Printf("curl http://localhost:41112/localapi/v0/status\n") printf("curl http://localhost:41112/localapi/v0/status\n")
return nil return nil
} }
fmt.Printf("curl --unix-socket %s http://foo/localapi/v0/status\n", paths.DefaultTailscaledSocket()) printf("curl --unix-socket %s http://foo/localapi/v0/status\n", paths.DefaultTailscaledSocket())
return nil return nil
} }
if out := debugArgs.cpuFile; out != "" { if out := debugArgs.cpuFile; out != "" {
@ -128,10 +128,10 @@ func runDebug(ctx context.Context, args []string) error {
return err return err
} }
if debugArgs.pretty { if debugArgs.pretty {
fmt.Println(prefs.Pretty()) outln(prefs.Pretty())
} else { } else {
j, _ := json.MarshalIndent(prefs, "", "\t") j, _ := json.MarshalIndent(prefs, "", "\t")
fmt.Println(string(j)) outln(string(j))
} }
return nil return nil
} }
@ -140,7 +140,7 @@ func runDebug(ctx context.Context, args []string) error {
if err != nil { if err != nil {
return err return err
} }
os.Stdout.Write(goroutines) Stdout.Write(goroutines)
return nil return nil
} }
if debugArgs.derpMap { if debugArgs.derpMap {
@ -150,7 +150,7 @@ func runDebug(ctx context.Context, args []string) error {
"failed to get local derp map, instead `curl %s/derpmap/default`: %w", ipn.DefaultControlURL, err, "failed to get local derp map, instead `curl %s/derpmap/default`: %w", ipn.DefaultControlURL, err,
) )
} }
enc := json.NewEncoder(os.Stdout) enc := json.NewEncoder(Stdout)
enc.SetIndent("", "\t") enc.SetIndent("", "\t")
enc.Encode(dm) enc.Encode(dm)
return nil return nil
@ -164,7 +164,7 @@ func runDebug(ctx context.Context, args []string) error {
n.NetMap = nil n.NetMap = nil
} }
j, _ := json.MarshalIndent(n, "", "\t") j, _ := json.MarshalIndent(n, "", "\t")
fmt.Printf("%s\n", j) printf("%s\n", j)
}) })
bc.RequestEngineStatus() bc.RequestEngineStatus()
pump(ctx, bc, c) pump(ctx, bc, c)
@ -176,7 +176,7 @@ func runDebug(ctx context.Context, args []string) error {
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
e := json.NewEncoder(os.Stdout) e := json.NewEncoder(Stdout)
e.SetIndent("", "\t") e.SetIndent("", "\t")
e.Encode(wfs) e.Encode(wfs)
return nil return nil
@ -190,7 +190,7 @@ func runDebug(ctx context.Context, args []string) error {
return err return err
} }
log.Printf("Size: %v\n", size) log.Printf("Size: %v\n", size)
io.Copy(os.Stdout, rc) io.Copy(Stdout, rc)
return nil return nil
} }
return nil return nil

@ -8,7 +8,6 @@ import (
"context" "context"
"fmt" "fmt"
"log" "log"
"os"
"github.com/peterbourgon/ff/v3/ffcli" "github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale" "tailscale.com/client/tailscale"
@ -33,7 +32,7 @@ func runDown(ctx context.Context, args []string) error {
return fmt.Errorf("error fetching current status: %w", err) return fmt.Errorf("error fetching current status: %w", err)
} }
if st.BackendState == "Stopped" { if st.BackendState == "Stopped" {
fmt.Fprintf(os.Stderr, "Tailscale was already stopped.\n") fmt.Fprintf(Stderr, "Tailscale was already stopped.\n")
return nil return nil
} }
_, err = tailscale.EditPrefs(ctx, &ipn.MaskedPrefs{ _, err = tailscale.EditPrefs(ctx, &ipn.MaskedPrefs{

@ -101,7 +101,7 @@ func runCp(ctx context.Context, args []string) error {
return fmt.Errorf("can't send to %s: %v", target, err) return fmt.Errorf("can't send to %s: %v", target, err)
} }
if isOffline { if isOffline {
fmt.Fprintf(os.Stderr, "# warning: %s is offline\n", target) fmt.Fprintf(Stderr, "# warning: %s is offline\n", target)
} }
if len(files) > 1 { if len(files) > 1 {
@ -172,7 +172,7 @@ func runCp(ctx context.Context, args []string) error {
res.Body.Close() res.Body.Close()
continue continue
} }
io.Copy(os.Stdout, res.Body) io.Copy(Stdout, res.Body)
res.Body.Close() res.Body.Close()
return errors.New(res.Status) return errors.New(res.Status)
} }
@ -293,7 +293,7 @@ func runCpTargets(ctx context.Context, args []string) error {
if detail != "" { if detail != "" {
detail = "\t" + detail detail = "\t" + detail
} }
fmt.Printf("%s\t%s%s\n", n.Addresses[0].IP(), n.ComputedName, detail) printf("%s\t%s%s\n", n.Addresses[0].IP(), n.ComputedName, detail)
} }
return nil return nil
} }

@ -75,7 +75,7 @@ func runIP(ctx context.Context, args []string) error {
for _, ip := range ips { for _, ip := range ips {
if ip.Is4() && v4 || ip.Is6() && v6 { if ip.Is4() && v4 || ip.Is6() && v6 {
match = true match = true
fmt.Println(ip) outln(ip)
} }
} }
if !match { if !match {

@ -60,7 +60,7 @@ func runNetcheck(ctx context.Context, args []string) error {
} }
if strings.HasPrefix(netcheckArgs.format, "json") { if strings.HasPrefix(netcheckArgs.format, "json") {
fmt.Fprintln(os.Stderr, "# Warning: this JSON format is not yet considered a stable interface") fmt.Fprintln(Stderr, "# Warning: this JSON format is not yet considered a stable interface")
} }
dm, err := tailscale.CurrentDERPMap(ctx) dm, err := tailscale.CurrentDERPMap(ctx)
@ -112,36 +112,36 @@ func printReport(dm *tailcfg.DERPMap, report *netcheck.Report) error {
} }
if j != nil { if j != nil {
j = append(j, '\n') j = append(j, '\n')
os.Stdout.Write(j) Stdout.Write(j)
return nil return nil
} }
fmt.Printf("\nReport:\n") printf("\nReport:\n")
fmt.Printf("\t* UDP: %v\n", report.UDP) printf("\t* UDP: %v\n", report.UDP)
if report.GlobalV4 != "" { if report.GlobalV4 != "" {
fmt.Printf("\t* IPv4: yes, %v\n", report.GlobalV4) printf("\t* IPv4: yes, %v\n", report.GlobalV4)
} else { } else {
fmt.Printf("\t* IPv4: (no addr found)\n") printf("\t* IPv4: (no addr found)\n")
} }
if report.GlobalV6 != "" { if report.GlobalV6 != "" {
fmt.Printf("\t* IPv6: yes, %v\n", report.GlobalV6) printf("\t* IPv6: yes, %v\n", report.GlobalV6)
} else if report.IPv6 { } else if report.IPv6 {
fmt.Printf("\t* IPv6: (no addr found)\n") printf("\t* IPv6: (no addr found)\n")
} else { } else {
fmt.Printf("\t* IPv6: no\n") printf("\t* IPv6: no\n")
} }
fmt.Printf("\t* MappingVariesByDestIP: %v\n", report.MappingVariesByDestIP) printf("\t* MappingVariesByDestIP: %v\n", report.MappingVariesByDestIP)
fmt.Printf("\t* HairPinning: %v\n", report.HairPinning) printf("\t* HairPinning: %v\n", report.HairPinning)
fmt.Printf("\t* PortMapping: %v\n", portMapping(report)) printf("\t* PortMapping: %v\n", portMapping(report))
// When DERP latency checking failed, // When DERP latency checking failed,
// magicsock will try to pick the DERP server that // magicsock will try to pick the DERP server that
// most of your other nodes are also using // most of your other nodes are also using
if len(report.RegionLatency) == 0 { if len(report.RegionLatency) == 0 {
fmt.Printf("\t* Nearest DERP: unknown (no response to latency probes)\n") printf("\t* Nearest DERP: unknown (no response to latency probes)\n")
} else { } else {
fmt.Printf("\t* Nearest DERP: %v\n", dm.Regions[report.PreferredDERP].RegionName) printf("\t* Nearest DERP: %v\n", dm.Regions[report.PreferredDERP].RegionName)
fmt.Printf("\t* DERP latency:\n") printf("\t* DERP latency:\n")
var rids []int var rids []int
for rid := range dm.Regions { for rid := range dm.Regions {
rids = append(rids, rid) rids = append(rids, rid)
@ -168,7 +168,7 @@ func printReport(dm *tailcfg.DERPMap, report *netcheck.Report) error {
if netcheckArgs.verbose { if netcheckArgs.verbose {
derpNum = fmt.Sprintf("derp%d, ", rid) derpNum = fmt.Sprintf("derp%d, ", rid)
} }
fmt.Printf("\t\t- %3s: %-7s (%s%s)\n", r.RegionCode, latency, derpNum, r.RegionName) printf("\t\t- %3s: %-7s (%s%s)\n", r.RegionCode, latency, derpNum, r.RegionName)
} }
} }
return nil return nil

@ -89,7 +89,7 @@ func runPing(ctx context.Context, args []string) error {
return err return err
} }
if self { if self {
fmt.Printf("%v is local Tailscale IP\n", ip) printf("%v is local Tailscale IP\n", ip)
return nil return nil
} }
@ -105,14 +105,14 @@ func runPing(ctx context.Context, args []string) error {
timer := time.NewTimer(pingArgs.timeout) timer := time.NewTimer(pingArgs.timeout)
select { select {
case <-timer.C: case <-timer.C:
fmt.Printf("timeout waiting for ping reply\n") printf("timeout waiting for ping reply\n")
case err := <-pumpErr: case err := <-pumpErr:
return err return err
case pr := <-prc: case pr := <-prc:
timer.Stop() timer.Stop()
if pr.Err != "" { if pr.Err != "" {
if pr.IsLocalIP { if pr.IsLocalIP {
fmt.Println(pr.Err) outln(pr.Err)
return nil return nil
} }
return errors.New(pr.Err) return errors.New(pr.Err)
@ -132,7 +132,7 @@ func runPing(ctx context.Context, args []string) error {
if pr.PeerAPIPort != 0 { if pr.PeerAPIPort != 0 {
extra = fmt.Sprintf(", %d", pr.PeerAPIPort) extra = fmt.Sprintf(", %d", pr.PeerAPIPort)
} }
fmt.Printf("pong from %s (%s%s) via %v in %v\n", pr.NodeName, pr.NodeIP, extra, via, latency) printf("pong from %s (%s%s) via %v in %v\n", pr.NodeName, pr.NodeIP, extra, via, latency)
if pingArgs.tsmp { if pingArgs.tsmp {
return nil return nil
} }

@ -70,7 +70,7 @@ func runStatus(ctx context.Context, args []string) error {
if err != nil { if err != nil {
return err return err
} }
fmt.Printf("%s", j) printf("%s", j)
return nil return nil
} }
if statusArgs.web { if statusArgs.web {
@ -79,7 +79,7 @@ func runStatus(ctx context.Context, args []string) error {
return err return err
} }
statusURL := interfaces.HTTPOfListener(ln) statusURL := interfaces.HTTPOfListener(ln)
fmt.Printf("Serving Tailscale status at %v ...\n", statusURL) printf("Serving Tailscale status at %v ...\n", statusURL)
go func() { go func() {
<-ctx.Done() <-ctx.Done()
ln.Close() ln.Close()
@ -108,30 +108,30 @@ func runStatus(ctx context.Context, args []string) error {
switch st.BackendState { switch st.BackendState {
default: default:
fmt.Fprintf(os.Stderr, "unexpected state: %s\n", st.BackendState) fmt.Fprintf(Stderr, "unexpected state: %s\n", st.BackendState)
os.Exit(1) os.Exit(1)
case ipn.Stopped.String(): case ipn.Stopped.String():
fmt.Println("Tailscale is stopped.") outln("Tailscale is stopped.")
os.Exit(1) os.Exit(1)
case ipn.NeedsLogin.String(): case ipn.NeedsLogin.String():
fmt.Println("Logged out.") outln("Logged out.")
if st.AuthURL != "" { if st.AuthURL != "" {
fmt.Printf("\nLog in at: %s\n", st.AuthURL) printf("\nLog in at: %s\n", st.AuthURL)
} }
os.Exit(1) os.Exit(1)
case ipn.NeedsMachineAuth.String(): case ipn.NeedsMachineAuth.String():
fmt.Println("Machine is not yet authorized by tailnet admin.") outln("Machine is not yet authorized by tailnet admin.")
os.Exit(1) os.Exit(1)
case ipn.Running.String(), ipn.Starting.String(): case ipn.Running.String(), ipn.Starting.String():
// Run below. // Run below.
} }
if len(st.Health) > 0 { if len(st.Health) > 0 {
fmt.Printf("# Health check:\n") printf("# Health check:\n")
for _, m := range st.Health { for _, m := range st.Health {
fmt.Printf("# - %s\n", m) printf("# - %s\n", m)
} }
fmt.Println() outln()
} }
var buf bytes.Buffer var buf bytes.Buffer
@ -190,7 +190,7 @@ func runStatus(ctx context.Context, args []string) error {
printPS(ps) printPS(ps)
} }
} }
os.Stdout.Write(buf.Bytes()) Stdout.Write(buf.Bytes())
return nil return nil
} }

@ -139,7 +139,7 @@ func (a upArgsT) getAuthKey() (string, error) {
var upArgs upArgsT var upArgs upArgsT
func warnf(format string, args ...interface{}) { func warnf(format string, args ...interface{}) {
fmt.Printf("Warning: "+format+"\n", args...) printf("Warning: "+format+"\n", args...)
} }
var ( var (
@ -435,12 +435,12 @@ func runUp(ctx context.Context, args []string) error {
startLoginInteractive() startLoginInteractive()
case ipn.NeedsMachineAuth: case ipn.NeedsMachineAuth:
printed = true printed = true
fmt.Fprintf(os.Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL()) fmt.Fprintf(Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL())
case ipn.Running: case ipn.Running:
// Done full authentication process // Done full authentication process
if printed { if printed {
// Only need to print an update if we printed the "please click" message earlier. // Only need to print an update if we printed the "please click" message earlier.
fmt.Fprintf(os.Stderr, "Success.\n") fmt.Fprintf(Stderr, "Success.\n")
} }
select { select {
case running <- true: case running <- true:
@ -451,13 +451,13 @@ func runUp(ctx context.Context, args []string) error {
} }
if url := n.BrowseToURL; url != nil && printAuthURL(*url) { if url := n.BrowseToURL; url != nil && printAuthURL(*url) {
printed = true printed = true
fmt.Fprintf(os.Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url) fmt.Fprintf(Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url)
if upArgs.qr { if upArgs.qr {
q, err := qrcode.New(*url, qrcode.Medium) q, err := qrcode.New(*url, qrcode.Medium)
if err != nil { if err != nil {
log.Printf("QR code error: %v", err) log.Printf("QR code error: %v", err)
} else { } else {
fmt.Fprintf(os.Stderr, "%s\n", q.ToString(false)) fmt.Fprintf(Stderr, "%s\n", q.ToString(false))
} }
} }

@ -7,7 +7,6 @@ package cli
import ( import (
"context" "context"
"flag" "flag"
"fmt"
"log" "log"
"github.com/peterbourgon/ff/v3/ffcli" "github.com/peterbourgon/ff/v3/ffcli"
@ -36,16 +35,16 @@ func runVersion(ctx context.Context, args []string) error {
log.Fatalf("too many non-flag arguments: %q", args) log.Fatalf("too many non-flag arguments: %q", args)
} }
if !versionArgs.daemon { if !versionArgs.daemon {
fmt.Println(version.String()) outln(version.String())
return nil return nil
} }
fmt.Printf("Client: %s\n", version.String()) printf("Client: %s\n", version.String())
st, err := tailscale.StatusWithoutPeers(ctx) st, err := tailscale.StatusWithoutPeers(ctx)
if err != nil { if err != nil {
return err return err
} }
fmt.Printf("Daemon: %s\n", st.Version) printf("Daemon: %s\n", st.Version)
return nil return nil
} }

Loading…
Cancel
Save