From 5df7ac70d6a942d1bd4cb5b388aecbe161a1a0d1 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Wed, 27 Oct 2021 14:53:46 -0700 Subject: [PATCH] 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 --- cmd/tailscale/cli/bugreport.go | 3 +-- cmd/tailscale/cli/cert.go | 4 ++-- cmd/tailscale/cli/cli.go | 22 ++++++++++++++++++++-- cmd/tailscale/cli/debug.go | 24 ++++++++++++------------ cmd/tailscale/cli/down.go | 3 +-- cmd/tailscale/cli/file.go | 6 +++--- cmd/tailscale/cli/ip.go | 2 +- cmd/tailscale/cli/netcheck.go | 32 ++++++++++++++++---------------- cmd/tailscale/cli/ping.go | 8 ++++---- cmd/tailscale/cli/status.go | 22 +++++++++++----------- cmd/tailscale/cli/up.go | 10 +++++----- cmd/tailscale/cli/version.go | 7 +++---- 12 files changed, 79 insertions(+), 64 deletions(-) diff --git a/cmd/tailscale/cli/bugreport.go b/cmd/tailscale/cli/bugreport.go index 1ca0747e6..5de2f134a 100644 --- a/cmd/tailscale/cli/bugreport.go +++ b/cmd/tailscale/cli/bugreport.go @@ -7,7 +7,6 @@ package cli import ( "context" "errors" - "fmt" "github.com/peterbourgon/ff/v3/ffcli" "tailscale.com/client/tailscale" @@ -33,6 +32,6 @@ func runBugReport(ctx context.Context, args []string) error { if err != nil { return err } - fmt.Println(logMarker) + outln(logMarker) return nil } diff --git a/cmd/tailscale/cli/cert.go b/cmd/tailscale/cli/cert.go index 2011f73d3..fa2a3e4fb 100644 --- a/cmd/tailscale/cli/cert.go +++ b/cmd/tailscale/cli/cert.go @@ -81,7 +81,7 @@ func runCert(ctx context.Context, args []string) error { domain := args[0] printf := func(format string, a ...interface{}) { - fmt.Printf(format, a...) + printf(format, a...) } if certArgs.certFile == "-" || certArgs.keyFile == "-" { 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) { if filename == "-" { - os.Stdout.Write(contents) + Stdout.Write(contents) return false, nil } if old, err := os.ReadFile(filename); err == nil && bytes.Equal(contents, old) { diff --git a/cmd/tailscale/cli/cli.go b/cmd/tailscale/cli/cli.go index 369995aeb..99d2d694f 100644 --- a/cmd/tailscale/cli/cli.go +++ b/cmd/tailscale/cli/cli.go @@ -31,6 +31,22 @@ import ( "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 // CLI based on os.Args, GOOS, the context the process is running in // (pty, parent PID), etc. @@ -82,7 +98,9 @@ func newFlagSet(name string) *flag.FlagSet { if runtime.GOOS == "js" { 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. @@ -94,7 +112,7 @@ func Run(args []string) error { var warnOnce sync.Once tailscale.SetVersionMismatchHandler(func(clientVer, serverVer string) { 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) }) }) diff --git a/cmd/tailscale/cli/debug.go b/cmd/tailscale/cli/debug.go index 7716863ef..3eb8179f6 100644 --- a/cmd/tailscale/cli/debug.go +++ b/cmd/tailscale/cli/debug.go @@ -61,7 +61,7 @@ var debugArgs struct { func writeProfile(dst string, v []byte) error { if dst == "-" { - _, err := os.Stdout.Write(v) + _, err := Stdout.Write(v) return err } return os.WriteFile(dst, v, 0600) @@ -83,21 +83,21 @@ func runDebug(ctx context.Context, args []string) error { } if debugArgs.env { for _, e := range os.Environ() { - fmt.Println(e) + outln(e) } return nil } if debugArgs.localCreds { port, token, err := safesocket.LocalTCPPortAndToken() 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 } 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 } - 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 } if out := debugArgs.cpuFile; out != "" { @@ -128,10 +128,10 @@ func runDebug(ctx context.Context, args []string) error { return err } if debugArgs.pretty { - fmt.Println(prefs.Pretty()) + outln(prefs.Pretty()) } else { j, _ := json.MarshalIndent(prefs, "", "\t") - fmt.Println(string(j)) + outln(string(j)) } return nil } @@ -140,7 +140,7 @@ func runDebug(ctx context.Context, args []string) error { if err != nil { return err } - os.Stdout.Write(goroutines) + Stdout.Write(goroutines) return nil } 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, ) } - enc := json.NewEncoder(os.Stdout) + enc := json.NewEncoder(Stdout) enc.SetIndent("", "\t") enc.Encode(dm) return nil @@ -164,7 +164,7 @@ func runDebug(ctx context.Context, args []string) error { n.NetMap = nil } j, _ := json.MarshalIndent(n, "", "\t") - fmt.Printf("%s\n", j) + printf("%s\n", j) }) bc.RequestEngineStatus() pump(ctx, bc, c) @@ -176,7 +176,7 @@ func runDebug(ctx context.Context, args []string) error { if err != nil { log.Fatal(err) } - e := json.NewEncoder(os.Stdout) + e := json.NewEncoder(Stdout) e.SetIndent("", "\t") e.Encode(wfs) return nil @@ -190,7 +190,7 @@ func runDebug(ctx context.Context, args []string) error { return err } log.Printf("Size: %v\n", size) - io.Copy(os.Stdout, rc) + io.Copy(Stdout, rc) return nil } return nil diff --git a/cmd/tailscale/cli/down.go b/cmd/tailscale/cli/down.go index 0f5d49101..90c568bd0 100644 --- a/cmd/tailscale/cli/down.go +++ b/cmd/tailscale/cli/down.go @@ -8,7 +8,6 @@ import ( "context" "fmt" "log" - "os" "github.com/peterbourgon/ff/v3/ffcli" "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) } if st.BackendState == "Stopped" { - fmt.Fprintf(os.Stderr, "Tailscale was already stopped.\n") + fmt.Fprintf(Stderr, "Tailscale was already stopped.\n") return nil } _, err = tailscale.EditPrefs(ctx, &ipn.MaskedPrefs{ diff --git a/cmd/tailscale/cli/file.go b/cmd/tailscale/cli/file.go index 1476528a8..3c0b01a57 100644 --- a/cmd/tailscale/cli/file.go +++ b/cmd/tailscale/cli/file.go @@ -101,7 +101,7 @@ func runCp(ctx context.Context, args []string) error { return fmt.Errorf("can't send to %s: %v", target, err) } 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 { @@ -172,7 +172,7 @@ func runCp(ctx context.Context, args []string) error { res.Body.Close() continue } - io.Copy(os.Stdout, res.Body) + io.Copy(Stdout, res.Body) res.Body.Close() return errors.New(res.Status) } @@ -293,7 +293,7 @@ func runCpTargets(ctx context.Context, args []string) error { if 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 } diff --git a/cmd/tailscale/cli/ip.go b/cmd/tailscale/cli/ip.go index 457d51916..521a64239 100644 --- a/cmd/tailscale/cli/ip.go +++ b/cmd/tailscale/cli/ip.go @@ -75,7 +75,7 @@ func runIP(ctx context.Context, args []string) error { for _, ip := range ips { if ip.Is4() && v4 || ip.Is6() && v6 { match = true - fmt.Println(ip) + outln(ip) } } if !match { diff --git a/cmd/tailscale/cli/netcheck.go b/cmd/tailscale/cli/netcheck.go index 6beb4f405..f0b33544e 100644 --- a/cmd/tailscale/cli/netcheck.go +++ b/cmd/tailscale/cli/netcheck.go @@ -60,7 +60,7 @@ func runNetcheck(ctx context.Context, args []string) error { } 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) @@ -112,36 +112,36 @@ func printReport(dm *tailcfg.DERPMap, report *netcheck.Report) error { } if j != nil { j = append(j, '\n') - os.Stdout.Write(j) + Stdout.Write(j) return nil } - fmt.Printf("\nReport:\n") - fmt.Printf("\t* UDP: %v\n", report.UDP) + printf("\nReport:\n") + printf("\t* UDP: %v\n", report.UDP) if report.GlobalV4 != "" { - fmt.Printf("\t* IPv4: yes, %v\n", report.GlobalV4) + printf("\t* IPv4: yes, %v\n", report.GlobalV4) } else { - fmt.Printf("\t* IPv4: (no addr found)\n") + printf("\t* IPv4: (no addr found)\n") } if report.GlobalV6 != "" { - fmt.Printf("\t* IPv6: yes, %v\n", report.GlobalV6) + printf("\t* IPv6: yes, %v\n", report.GlobalV6) } else if report.IPv6 { - fmt.Printf("\t* IPv6: (no addr found)\n") + printf("\t* IPv6: (no addr found)\n") } else { - fmt.Printf("\t* IPv6: no\n") + printf("\t* IPv6: no\n") } - fmt.Printf("\t* MappingVariesByDestIP: %v\n", report.MappingVariesByDestIP) - fmt.Printf("\t* HairPinning: %v\n", report.HairPinning) - fmt.Printf("\t* PortMapping: %v\n", portMapping(report)) + printf("\t* MappingVariesByDestIP: %v\n", report.MappingVariesByDestIP) + printf("\t* HairPinning: %v\n", report.HairPinning) + printf("\t* PortMapping: %v\n", portMapping(report)) // When DERP latency checking failed, // magicsock will try to pick the DERP server that // most of your other nodes are also using 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 { - fmt.Printf("\t* Nearest DERP: %v\n", dm.Regions[report.PreferredDERP].RegionName) - fmt.Printf("\t* DERP latency:\n") + printf("\t* Nearest DERP: %v\n", dm.Regions[report.PreferredDERP].RegionName) + printf("\t* DERP latency:\n") var rids []int for rid := range dm.Regions { rids = append(rids, rid) @@ -168,7 +168,7 @@ func printReport(dm *tailcfg.DERPMap, report *netcheck.Report) error { if netcheckArgs.verbose { 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 diff --git a/cmd/tailscale/cli/ping.go b/cmd/tailscale/cli/ping.go index ef6382e1d..8953ff09d 100644 --- a/cmd/tailscale/cli/ping.go +++ b/cmd/tailscale/cli/ping.go @@ -89,7 +89,7 @@ func runPing(ctx context.Context, args []string) error { return err } if self { - fmt.Printf("%v is local Tailscale IP\n", ip) + printf("%v is local Tailscale IP\n", ip) return nil } @@ -105,14 +105,14 @@ func runPing(ctx context.Context, args []string) error { timer := time.NewTimer(pingArgs.timeout) select { case <-timer.C: - fmt.Printf("timeout waiting for ping reply\n") + printf("timeout waiting for ping reply\n") case err := <-pumpErr: return err case pr := <-prc: timer.Stop() if pr.Err != "" { if pr.IsLocalIP { - fmt.Println(pr.Err) + outln(pr.Err) return nil } return errors.New(pr.Err) @@ -132,7 +132,7 @@ func runPing(ctx context.Context, args []string) error { if pr.PeerAPIPort != 0 { 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 { return nil } diff --git a/cmd/tailscale/cli/status.go b/cmd/tailscale/cli/status.go index 99a3b6d6b..e5e230590 100644 --- a/cmd/tailscale/cli/status.go +++ b/cmd/tailscale/cli/status.go @@ -70,7 +70,7 @@ func runStatus(ctx context.Context, args []string) error { if err != nil { return err } - fmt.Printf("%s", j) + printf("%s", j) return nil } if statusArgs.web { @@ -79,7 +79,7 @@ func runStatus(ctx context.Context, args []string) error { return err } statusURL := interfaces.HTTPOfListener(ln) - fmt.Printf("Serving Tailscale status at %v ...\n", statusURL) + printf("Serving Tailscale status at %v ...\n", statusURL) go func() { <-ctx.Done() ln.Close() @@ -108,30 +108,30 @@ func runStatus(ctx context.Context, args []string) error { switch st.BackendState { default: - fmt.Fprintf(os.Stderr, "unexpected state: %s\n", st.BackendState) + fmt.Fprintf(Stderr, "unexpected state: %s\n", st.BackendState) os.Exit(1) case ipn.Stopped.String(): - fmt.Println("Tailscale is stopped.") + outln("Tailscale is stopped.") os.Exit(1) case ipn.NeedsLogin.String(): - fmt.Println("Logged out.") + outln("Logged out.") if st.AuthURL != "" { - fmt.Printf("\nLog in at: %s\n", st.AuthURL) + printf("\nLog in at: %s\n", st.AuthURL) } os.Exit(1) 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) case ipn.Running.String(), ipn.Starting.String(): // Run below. } if len(st.Health) > 0 { - fmt.Printf("# Health check:\n") + printf("# Health check:\n") for _, m := range st.Health { - fmt.Printf("# - %s\n", m) + printf("# - %s\n", m) } - fmt.Println() + outln() } var buf bytes.Buffer @@ -190,7 +190,7 @@ func runStatus(ctx context.Context, args []string) error { printPS(ps) } } - os.Stdout.Write(buf.Bytes()) + Stdout.Write(buf.Bytes()) return nil } diff --git a/cmd/tailscale/cli/up.go b/cmd/tailscale/cli/up.go index 3adaa3600..ed0dbfce0 100644 --- a/cmd/tailscale/cli/up.go +++ b/cmd/tailscale/cli/up.go @@ -139,7 +139,7 @@ func (a upArgsT) getAuthKey() (string, error) { var upArgs upArgsT func warnf(format string, args ...interface{}) { - fmt.Printf("Warning: "+format+"\n", args...) + printf("Warning: "+format+"\n", args...) } var ( @@ -435,12 +435,12 @@ func runUp(ctx context.Context, args []string) error { startLoginInteractive() case ipn.NeedsMachineAuth: 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: // Done full authentication process if printed { // 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 { case running <- true: @@ -451,13 +451,13 @@ func runUp(ctx context.Context, args []string) error { } if url := n.BrowseToURL; url != nil && printAuthURL(*url) { 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 { q, err := qrcode.New(*url, qrcode.Medium) if err != nil { log.Printf("QR code error: %v", err) } else { - fmt.Fprintf(os.Stderr, "%s\n", q.ToString(false)) + fmt.Fprintf(Stderr, "%s\n", q.ToString(false)) } } diff --git a/cmd/tailscale/cli/version.go b/cmd/tailscale/cli/version.go index fca0b3c9d..1726bac0b 100644 --- a/cmd/tailscale/cli/version.go +++ b/cmd/tailscale/cli/version.go @@ -7,7 +7,6 @@ package cli import ( "context" "flag" - "fmt" "log" "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) } if !versionArgs.daemon { - fmt.Println(version.String()) + outln(version.String()) return nil } - fmt.Printf("Client: %s\n", version.String()) + printf("Client: %s\n", version.String()) st, err := tailscale.StatusWithoutPeers(ctx) if err != nil { return err } - fmt.Printf("Daemon: %s\n", st.Version) + printf("Daemon: %s\n", st.Version) return nil }