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 (
"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
}

@ -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) {

@ -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)
})
})

@ -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

@ -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{

@ -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
}

@ -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 {

@ -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

@ -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
}

@ -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
}

@ -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))
}
}

@ -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
}

Loading…
Cancel
Save