Merge pull request #26665 from hashicorp/jbardin/logging

Restore "crash.log" behavior and remove prefixedio
This commit is contained in:
James Bardin 2020-10-22 10:34:51 -04:00 committed by GitHub
commit bc1a841d65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 78 additions and 134 deletions

View File

@ -256,19 +256,11 @@ func TestInitProviders_pluginCache(t *testing.T) {
// convert the slashes if building for windows. // convert the slashes if building for windows.
p := filepath.FromSlash("./cache") p := filepath.FromSlash("./cache")
cmd.Env = append(cmd.Env, "TF_PLUGIN_CACHE_DIR="+p) cmd.Env = append(cmd.Env, "TF_PLUGIN_CACHE_DIR="+p)
cmd.Stdin = nil
cmd.Stderr = &bytes.Buffer{}
err = cmd.Run() err = cmd.Run()
if err != nil { if err != nil {
t.Errorf("unexpected error: %s", err) t.Errorf("unexpected error: %s", err)
} }
stderr := cmd.Stderr.(*bytes.Buffer).String()
if stderr != "" {
t.Errorf("unexpected stderr output:\n%s\n", stderr)
}
path := filepath.FromSlash(fmt.Sprintf(".terraform/providers/registry.terraform.io/hashicorp/template/2.1.0/%s_%s/terraform-provider-template_v2.1.0_x4", runtime.GOOS, runtime.GOARCH)) path := filepath.FromSlash(fmt.Sprintf(".terraform/providers/registry.terraform.io/hashicorp/template/2.1.0/%s_%s/terraform-provider-template_v2.1.0_x4", runtime.GOOS, runtime.GOARCH))
content, err := tf.ReadFile(path) content, err := tf.ReadFile(path)
if err != nil { if err != nil {
@ -381,13 +373,13 @@ func TestInitProviderWarnings(t *testing.T) {
tf := e2e.NewBinary(terraformBin, fixturePath) tf := e2e.NewBinary(terraformBin, fixturePath)
defer tf.Close() defer tf.Close()
stdout, _, err := tf.Run("init") _, stderr, err := tf.Run("init")
if err == nil { if err == nil {
t.Fatal("expected error, got success") t.Fatal("expected error, got success")
} }
if !strings.Contains(stdout, "This provider is archived and no longer needed. The terraform_remote_state\ndata source is built into the latest Terraform release.") { if !strings.Contains(stderr, "This provider is archived and no longer needed. The terraform_remote_state\ndata source is built into the latest Terraform release.") {
t.Errorf("expected warning message is missing from output:\n%s", stdout) t.Errorf("expected warning message is missing from output:\n%s", stderr)
} }
} }

View File

@ -70,7 +70,7 @@ func TestProviderDevOverrides(t *testing.T) {
if got, want := stdout, `The configuration is valid, but`; !strings.Contains(got, want) { if got, want := stdout, `The configuration is valid, but`; !strings.Contains(got, want) {
t.Errorf("stdout doesn't include the success message\nwant: %s\n%s", want, got) t.Errorf("stdout doesn't include the success message\nwant: %s\n%s", want, got)
} }
if got, want := stdout, `Provider development overrides are in effect`; !strings.Contains(got, want) { if got, want := stderr, `Provider development overrides are in effect`; !strings.Contains(got, want) {
t.Errorf("stdout doesn't include the warning about development overrides\nwant: %s\n%s", want, got) t.Errorf("stdout doesn't include the warning about development overrides\nwant: %s\n%s", want, got)
} }
} }

View File

@ -335,7 +335,7 @@ func providerFactory(meta *providercache.CachedProvider) providers.Factory {
config := &plugin.ClientConfig{ config := &plugin.ClientConfig{
HandshakeConfig: tfplugin.Handshake, HandshakeConfig: tfplugin.Handshake,
Logger: logging.NewHCLogger("plugin"), Logger: logging.NewProviderLogger(),
AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC}, AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
Managed: true, Managed: true,
Cmd: exec.Command(execFile), Cmd: exec.Command(execFile),

View File

@ -169,7 +169,7 @@ func internalPluginClient(kind, name string) (*plugin.Client, error) {
VersionedPlugins: tfplugin.VersionedPlugins, VersionedPlugins: tfplugin.VersionedPlugins,
AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC}, AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
AutoMTLS: enableProviderAutoMTLS, AutoMTLS: enableProviderAutoMTLS,
Logger: logging.NewHCLogger("plugin"), Logger: logging.NewProviderLogger(),
} }
return plugin.NewClient(cfg), nil return plugin.NewClient(cfg), nil

5
go.mod
View File

@ -55,7 +55,7 @@ require (
github.com/hashicorp/go-checkpoint v0.5.0 github.com/hashicorp/go-checkpoint v0.5.0
github.com/hashicorp/go-cleanhttp v0.5.1 github.com/hashicorp/go-cleanhttp v0.5.1
github.com/hashicorp/go-getter v1.4.2-0.20200106182914-9813cbd4eb02 github.com/hashicorp/go-getter v1.4.2-0.20200106182914-9813cbd4eb02
github.com/hashicorp/go-hclog v0.14.1 github.com/hashicorp/go-hclog v0.14.2-0.20201019183805-6b377870ae4a
github.com/hashicorp/go-immutable-radix v0.0.0-20180129170900-7f3cd4390caa // indirect github.com/hashicorp/go-immutable-radix v0.0.0-20180129170900-7f3cd4390caa // indirect
github.com/hashicorp/go-msgpack v0.5.4 // indirect github.com/hashicorp/go-msgpack v0.5.4 // indirect
github.com/hashicorp/go-multierror v1.0.0 github.com/hashicorp/go-multierror v1.0.0
@ -84,7 +84,7 @@ require (
github.com/lusis/go-artifactory v0.0.0-20160115162124-7e4ce345df82 github.com/lusis/go-artifactory v0.0.0-20160115162124-7e4ce345df82
github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 // indirect github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 // indirect
github.com/masterzen/winrm v0.0.0-20200615185753-c42b5136ff88 github.com/masterzen/winrm v0.0.0-20200615185753-c42b5136ff88
github.com/mattn/go-colorable v0.1.8 github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.12 github.com/mattn/go-isatty v0.0.12
github.com/mattn/go-shellwords v1.0.4 github.com/mattn/go-shellwords v1.0.4
github.com/miekg/dns v1.0.8 // indirect github.com/miekg/dns v1.0.8 // indirect
@ -97,7 +97,6 @@ require (
github.com/mitchellh/gox v1.0.1 github.com/mitchellh/gox v1.0.1
github.com/mitchellh/mapstructure v1.1.2 github.com/mitchellh/mapstructure v1.1.2
github.com/mitchellh/panicwrap v1.0.0 github.com/mitchellh/panicwrap v1.0.0
github.com/mitchellh/prefixedio v0.0.0-20190213213902-5733675afd51
github.com/mitchellh/reflectwalk v1.0.1 github.com/mitchellh/reflectwalk v1.0.1
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/packer-community/winrmcp v0.0.0-20180921211025-c76d91c1e7db github.com/packer-community/winrmcp v0.0.0-20180921211025-c76d91c1e7db

4
go.sum
View File

@ -240,6 +240,8 @@ github.com/hashicorp/go-getter v1.4.2-0.20200106182914-9813cbd4eb02/go.mod h1:7q
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
github.com/hashicorp/go-hclog v0.14.1 h1:nQcJDQwIAGnmoUWp8ubocEX40cCml/17YkF6csQLReU= github.com/hashicorp/go-hclog v0.14.1 h1:nQcJDQwIAGnmoUWp8ubocEX40cCml/17YkF6csQLReU=
github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v0.14.2-0.20201019183805-6b377870ae4a h1:jMdSTm5U2cMM2Z0y+tiSTsMAinefoMZyXGj3Gap+JmE=
github.com/hashicorp/go-hclog v0.14.2-0.20201019183805-6b377870ae4a/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-immutable-radix v0.0.0-20180129170900-7f3cd4390caa h1:0nA8i+6Rwqaq9xlpmVxxTwk6rxiEhX+E6Wh4vPNHiS8= github.com/hashicorp/go-immutable-radix v0.0.0-20180129170900-7f3cd4390caa h1:0nA8i+6Rwqaq9xlpmVxxTwk6rxiEhX+E6Wh4vPNHiS8=
github.com/hashicorp/go-immutable-radix v0.0.0-20180129170900-7f3cd4390caa/go.mod h1:6ij3Z20p+OhOkCSrA0gImAWoHYQRGbnlcuk6XYTiaRw= github.com/hashicorp/go-immutable-radix v0.0.0-20180129170900-7f3cd4390caa/go.mod h1:6ij3Z20p+OhOkCSrA0gImAWoHYQRGbnlcuk6XYTiaRw=
github.com/hashicorp/go-msgpack v0.5.4 h1:SFT72YqIkOcLdWJUYcriVX7hbrZpwc/f7h8aW2NUqrA= github.com/hashicorp/go-msgpack v0.5.4 h1:SFT72YqIkOcLdWJUYcriVX7hbrZpwc/f7h8aW2NUqrA=
@ -384,8 +386,6 @@ github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQz
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/panicwrap v1.0.0 h1:67zIyVakCIvcs69A0FGfZjBdPleaonSgGlXRSRlb6fE= github.com/mitchellh/panicwrap v1.0.0 h1:67zIyVakCIvcs69A0FGfZjBdPleaonSgGlXRSRlb6fE=
github.com/mitchellh/panicwrap v1.0.0/go.mod h1:pKvZHwWrZowLUzftuFq7coarnxbBXU4aQh3N0BJOeeA= github.com/mitchellh/panicwrap v1.0.0/go.mod h1:pKvZHwWrZowLUzftuFq7coarnxbBXU4aQh3N0BJOeeA=
github.com/mitchellh/prefixedio v0.0.0-20190213213902-5733675afd51 h1:eD92Am0Qf3rqhsOeA1zwBHSfRkoHrt4o6uORamdmJP8=
github.com/mitchellh/prefixedio v0.0.0-20190213213902-5733675afd51/go.mod h1:kB1naBgV9ORnkiTVeyJOI1DavaJkG4oNIq0Af6ZVKUo=
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE= github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE=

View File

@ -38,6 +38,23 @@ func init() {
log.SetOutput(logWriter) log.SetOutput(logWriter)
} }
// SetupTempLog adds a new log sink which writes all logs to the given file.
func RegisterSink(f *os.File) {
l, ok := logger.(hclog.InterceptLogger)
if !ok {
panic("global logger is not an InterceptLogger")
}
if f == nil {
return
}
l.RegisterSink(hclog.NewSinkAdapter(&hclog.LoggerOptions{
Level: hclog.Trace,
Output: f,
}))
}
// LogOutput return the default global log io.Writer // LogOutput return the default global log io.Writer
func LogOutput() io.Writer { func LogOutput() io.Writer {
return logWriter return logWriter
@ -65,13 +82,17 @@ func NewHCLogger(name string) hclog.Logger {
} }
} }
return hclog.New(&hclog.LoggerOptions{ return hclog.NewInterceptLogger(&hclog.LoggerOptions{
Name: name, Name: name,
Level: hclog.LevelFromString(logLevel), Level: hclog.LevelFromString(logLevel),
Output: logOutput, Output: logOutput,
}) })
} }
func NewProviderLogger() hclog.Logger {
return logger.Named("plugin")
}
// CurrentLogLevel returns the current log level string based the environment vars // CurrentLogLevel returns the current log level string based the environment vars
func CurrentLogLevel() string { func CurrentLogLevel() string {
envLevel := strings.ToUpper(os.Getenv(EnvLog)) envLevel := strings.ToUpper(os.Getenv(EnvLog))
@ -83,11 +104,8 @@ func CurrentLogLevel() string {
if isValidLogLevel(envLevel) { if isValidLogLevel(envLevel) {
logLevel = envLevel logLevel = envLevel
} else { } else {
log.Printf("[WARN] Invalid log level: %q. Defaulting to level: TRACE. Valid levels are: %+v", logger.Warn(fmt.Sprintf("Invalid log level: %q. Defaulting to level: TRACE. Valid levels are: %+v",
envLevel, ValidLevels) envLevel, ValidLevels))
}
if logLevel != "TRACE" {
log.Printf("[WARN] Log levels other than TRACE are currently unreliable, and are supported only for backward compatibility.\n Use TF_LOG=TRACE to see Terraform's internal logs.\n ----")
} }
return logLevel return logLevel

View File

@ -1,8 +1,9 @@
package main package logging
import ( import (
"fmt" "fmt"
"io" "io"
"io/ioutil"
"os" "os"
"strings" "strings"
@ -15,7 +16,7 @@ const panicOutput = `
!!!!!!!!!!!!!!!!!!!!!!!!!!! TERRAFORM CRASH !!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!! TERRAFORM CRASH !!!!!!!!!!!!!!!!!!!!!!!!!!!!
Terraform crashed! This is always indicative of a bug within Terraform. Terraform crashed! This is always indicative of a bug within Terraform.
A crash log has been placed at "crash.log" relative to your current A crash log has been placed at %[1]q relative to your current
working directory. It would be immensely helpful if you could please working directory. It would be immensely helpful if you could please
report the crash with Terraform[1] so that we can fix this. report the crash with Terraform[1] so that we can fix this.
@ -23,7 +24,7 @@ When reporting bugs, please include your terraform version. That
information is available on the first line of crash.log. You can also information is available on the first line of crash.log. You can also
get it by running 'terraform --version' on the command line. get it by running 'terraform --version' on the command line.
SECURITY WARNING: the "crash.log" file that was created may contain SECURITY WARNING: the %[1]q file that was created may contain
sensitive information that must be redacted before it is safe to share sensitive information that must be redacted before it is safe to share
on the issue tracker. on the issue tracker.
@ -36,36 +37,36 @@ on the issue tracker.
// within Terraform. It is guaranteed to run after the resulting process has // within Terraform. It is guaranteed to run after the resulting process has
// exited so we can take the log file, add in the panic, and store it // exited so we can take the log file, add in the panic, and store it
// somewhere locally. // somewhere locally.
func panicHandler(logF *os.File) panicwrap.HandlerFunc { func PanicHandler(tmpLogPath string) panicwrap.HandlerFunc {
return func(m string) { return func(m string) {
// Right away just output this thing on stderr so that it gets
// shown in case anything below fails.
fmt.Fprintf(os.Stderr, fmt.Sprintf("%s\n", m))
// Create the crash log file where we'll write the logs // Create the crash log file where we'll write the logs
f, err := os.Create("crash.log") f, err := ioutil.TempFile(".", "crash.*.log")
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create crash log file: %s", err) fmt.Fprintf(os.Stderr, "Failed to create crash log file: %s", err)
return return
} }
defer f.Close() defer f.Close()
// Seek the log file back to the beginning tmpLog, err := os.Open(tmpLogPath)
if _, err = logF.Seek(0, 0); err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Failed to seek log file for crash: %s", err) fmt.Fprintf(os.Stderr, "Failed to open log file %q: %v\n", tmpLogPath, err)
return return
} }
defer tmpLog.Close()
// Copy the contents to the crash file. This will include // Copy the contents to the crash file. This will include
// the panic that just happened. // the panic that just happened.
if _, err = io.Copy(f, logF); err != nil { if _, err = io.Copy(f, tmpLog); err != nil {
fmt.Fprintf(os.Stderr, "Failed to write crash log: %s", err) fmt.Fprintf(os.Stderr, "Failed to write crash log: %s", err)
return return
} }
// add the trace back to the log
f.WriteString("\n" + m)
// Tell the user a crash occurred in some helpful way that // Tell the user a crash occurred in some helpful way that
// they'll hopefully notice. // they'll hopefully notice.
fmt.Printf("\n\n") fmt.Printf("\n\n")
fmt.Println(strings.TrimSpace(panicOutput)) fmt.Printf(strings.TrimSpace(panicOutput), f.Name())
} }
} }

124
main.go
View File

@ -3,7 +3,6 @@ package main
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"log" "log"
"net" "net"
@ -11,7 +10,6 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
"sync"
"github.com/hashicorp/go-plugin" "github.com/hashicorp/go-plugin"
"github.com/hashicorp/terraform-svchost/disco" "github.com/hashicorp/terraform-svchost/disco"
@ -19,13 +17,12 @@ import (
"github.com/hashicorp/terraform/command/cliconfig" "github.com/hashicorp/terraform/command/cliconfig"
"github.com/hashicorp/terraform/command/format" "github.com/hashicorp/terraform/command/format"
"github.com/hashicorp/terraform/httpclient" "github.com/hashicorp/terraform/httpclient"
"github.com/hashicorp/terraform/internal/logging"
"github.com/hashicorp/terraform/version" "github.com/hashicorp/terraform/version"
"github.com/mattn/go-colorable"
"github.com/mattn/go-shellwords" "github.com/mattn/go-shellwords"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
"github.com/mitchellh/colorstring" "github.com/mitchellh/colorstring"
"github.com/mitchellh/panicwrap" "github.com/mitchellh/panicwrap"
"github.com/mitchellh/prefixedio"
backendInit "github.com/hashicorp/terraform/backend/init" backendInit "github.com/hashicorp/terraform/backend/init"
) )
@ -33,6 +30,9 @@ import (
const ( const (
// EnvCLI is the environment variable name to set additional CLI args. // EnvCLI is the environment variable name to set additional CLI args.
EnvCLI = "TF_CLI_ARGS" EnvCLI = "TF_CLI_ARGS"
// The parent process will create a file to collect crash logs
envTmpLogPath = "TF_TEMP_LOG_PATH"
) )
func main() { func main() {
@ -55,19 +55,16 @@ func realMain() int {
fmt.Fprintf(os.Stderr, "Couldn't setup logging tempfile: %s", err) fmt.Fprintf(os.Stderr, "Couldn't setup logging tempfile: %s", err)
return 1 return 1
} }
// Now that we have the file, close it and leave it for the wrapped
// process to write to.
logTempFile.Close()
defer os.Remove(logTempFile.Name()) defer os.Remove(logTempFile.Name())
defer logTempFile.Close()
// Setup the prefixed readers that send data properly to // store the path in the environment for the wrapped executable
// stdout/stderr. os.Setenv(envTmpLogPath, logTempFile.Name())
doneCh := make(chan struct{})
outR, outW := io.Pipe()
go copyOutput(outR, doneCh)
// Create the configuration for panicwrap and wrap our executable // Create the configuration for panicwrap and wrap our executable
wrapConfig.Handler = panicHandler(logTempFile) wrapConfig.Handler = logging.PanicHandler(logTempFile.Name())
wrapConfig.Writer = os.Stderr
wrapConfig.Stdout = outW
wrapConfig.IgnoreSignals = ignoreSignals wrapConfig.IgnoreSignals = ignoreSignals
wrapConfig.ForwardSignals = forwardSignals wrapConfig.ForwardSignals = forwardSignals
exitStatus, err := panicwrap.Wrap(&wrapConfig) exitStatus, err := panicwrap.Wrap(&wrapConfig)
@ -76,20 +73,7 @@ func realMain() int {
return 1 return 1
} }
// If >= 0, we're the parent, so just exit return exitStatus
if exitStatus >= 0 {
// Close the stdout writer so that our copy process can finish
outW.Close()
// Wait for the output copying to finish
<-doneCh
return exitStatus
}
// We're the child, so just close the tempfile we made in order to
// save file handles since the tempfile is only used by the parent.
logTempFile.Close()
} }
// Call the real main // Call the real main
@ -97,21 +81,30 @@ func realMain() int {
} }
func init() { func init() {
Ui = &cli.PrefixedUi{ Ui = &cli.BasicUi{
AskPrefix: OutputPrefix, Writer: os.Stdout,
OutputPrefix: OutputPrefix, ErrorWriter: os.Stderr,
InfoPrefix: OutputPrefix, Reader: os.Stdin,
ErrorPrefix: ErrorPrefix,
Ui: &cli.BasicUi{
Writer: os.Stdout,
Reader: os.Stdin,
},
} }
} }
func wrappedMain() int { func wrappedMain() int {
var err error var err error
tmpLogPath := os.Getenv(envTmpLogPath)
if tmpLogPath != "" {
f, err := os.OpenFile(tmpLogPath, os.O_RDWR|os.O_APPEND, 0666)
if err == nil {
defer os.Remove(f.Name())
defer f.Close()
log.Printf("[DEBUG] Adding temp file log sink: %s", f.Name())
logging.RegisterSink(f)
} else {
log.Printf("[ERROR] Could not open temp log file: %v", err)
}
}
log.Printf( log.Printf(
"[INFO] Terraform version: %s %s %s", "[INFO] Terraform version: %s %s %s",
Version, VersionPrerelease, GitCommit) Version, VersionPrerelease, GitCommit)
@ -299,65 +292,6 @@ func wrappedMain() int {
return exitCode return exitCode
} }
// copyOutput uses output prefixes to determine whether data on stdout
// should go to stdout or stderr. This is due to panicwrap using stderr
// as the log and error channel.
func copyOutput(r io.Reader, doneCh chan<- struct{}) {
defer close(doneCh)
pr, err := prefixedio.NewReader(r)
if err != nil {
panic(err)
}
stderrR, err := pr.Prefix(ErrorPrefix)
if err != nil {
panic(err)
}
stdoutR, err := pr.Prefix(OutputPrefix)
if err != nil {
panic(err)
}
defaultR, err := pr.Prefix("")
if err != nil {
panic(err)
}
var stdout io.Writer = os.Stdout
var stderr io.Writer = os.Stderr
if runtime.GOOS == "windows" {
stdout = colorable.NewColorableStdout()
stderr = colorable.NewColorableStderr()
// colorable is not concurrency-safe when stdout and stderr are the
// same console, so we need to add some synchronization to ensure that
// we can't be concurrently writing to both stderr and stdout at
// once, or else we get intermingled writes that create gibberish
// in the console.
wrapped := synchronizedWriters(stdout, stderr)
stdout = wrapped[0]
stderr = wrapped[1]
}
var wg sync.WaitGroup
wg.Add(3)
go func() {
defer wg.Done()
io.Copy(stderr, stderrR)
}()
go func() {
defer wg.Done()
io.Copy(stdout, stdoutR)
}()
go func() {
defer wg.Done()
io.Copy(stdout, defaultR)
}()
wg.Wait()
}
func mergeEnvArgs(envName string, cmd string, args []string) ([]string, error) { func mergeEnvArgs(envName string, cmd string, args []string) ([]string, error) {
v := os.Getenv(envName) v := os.Getenv(envName)
if v == "" { if v == "" {