github.com/StackPointCloud/packer@v0.10.2-0.20180716202532-b28098e0f79b/packer/ui.go (about)

     1  package packer
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"log"
    10  	"os"
    11  	"os/signal"
    12  	"runtime"
    13  	"strings"
    14  	"sync"
    15  	"syscall"
    16  	"time"
    17  	"unicode"
    18  )
    19  
    20  type UiColor uint
    21  
    22  const (
    23  	UiColorRed     UiColor = 31
    24  	UiColorGreen           = 32
    25  	UiColorYellow          = 33
    26  	UiColorBlue            = 34
    27  	UiColorMagenta         = 35
    28  	UiColorCyan            = 36
    29  )
    30  
    31  // The Ui interface handles all communication for Packer with the outside
    32  // world. This sort of control allows us to strictly control how output
    33  // is formatted and various levels of output.
    34  type Ui interface {
    35  	Ask(string) (string, error)
    36  	Say(string)
    37  	Message(string)
    38  	Error(string)
    39  	Machine(string, ...string)
    40  }
    41  
    42  // ColoredUi is a UI that is colored using terminal colors.
    43  type ColoredUi struct {
    44  	Color      UiColor
    45  	ErrorColor UiColor
    46  	Ui         Ui
    47  }
    48  
    49  // TargetedUI is a UI that wraps another UI implementation and modifies
    50  // the output to indicate a specific target. Specifically, all Say output
    51  // is prefixed with the target name. Message output is not prefixed but
    52  // is offset by the length of the target so that output is lined up properly
    53  // with Say output. Machine-readable output has the proper target set.
    54  type TargetedUI struct {
    55  	Target string
    56  	Ui     Ui
    57  }
    58  
    59  // The BasicUI is a UI that reads and writes from a standard Go reader
    60  // and writer. It is safe to be called from multiple goroutines. Machine
    61  // readable output is simply logged for this UI.
    62  type BasicUi struct {
    63  	Reader      io.Reader
    64  	Writer      io.Writer
    65  	ErrorWriter io.Writer
    66  	l           sync.Mutex
    67  	interrupted bool
    68  	scanner     *bufio.Scanner
    69  }
    70  
    71  // MachineReadableUi is a UI that only outputs machine-readable output
    72  // to the given Writer.
    73  type MachineReadableUi struct {
    74  	Writer io.Writer
    75  }
    76  
    77  func (u *ColoredUi) Ask(query string) (string, error) {
    78  	return u.Ui.Ask(u.colorize(query, u.Color, true))
    79  }
    80  
    81  func (u *ColoredUi) Say(message string) {
    82  	u.Ui.Say(u.colorize(message, u.Color, true))
    83  }
    84  
    85  func (u *ColoredUi) Message(message string) {
    86  	u.Ui.Message(u.colorize(message, u.Color, false))
    87  }
    88  
    89  func (u *ColoredUi) Error(message string) {
    90  	color := u.ErrorColor
    91  	if color == 0 {
    92  		color = UiColorRed
    93  	}
    94  
    95  	u.Ui.Error(u.colorize(message, color, true))
    96  }
    97  
    98  func (u *ColoredUi) Machine(t string, args ...string) {
    99  	// Don't colorize machine-readable output
   100  	u.Ui.Machine(t, args...)
   101  }
   102  
   103  func (u *ColoredUi) colorize(message string, color UiColor, bold bool) string {
   104  	if !u.supportsColors() {
   105  		return message
   106  	}
   107  
   108  	attr := 0
   109  	if bold {
   110  		attr = 1
   111  	}
   112  
   113  	return fmt.Sprintf("\033[%d;%dm%s\033[0m", attr, color, message)
   114  }
   115  
   116  func (u *ColoredUi) supportsColors() bool {
   117  	// Never use colors if we have this environmental variable
   118  	if os.Getenv("PACKER_NO_COLOR") != "" {
   119  		return false
   120  	}
   121  
   122  	// For now, on non-Windows machine, just assume it does
   123  	if runtime.GOOS != "windows" {
   124  		return true
   125  	}
   126  
   127  	// On Windows, if we appear to be in Cygwin, then it does
   128  	cygwin := os.Getenv("CYGWIN") != "" ||
   129  		os.Getenv("OSTYPE") == "cygwin" ||
   130  		os.Getenv("TERM") == "cygwin"
   131  
   132  	return cygwin
   133  }
   134  
   135  func (u *TargetedUI) Ask(query string) (string, error) {
   136  	return u.Ui.Ask(u.prefixLines(true, query))
   137  }
   138  
   139  func (u *TargetedUI) Say(message string) {
   140  	u.Ui.Say(u.prefixLines(true, message))
   141  }
   142  
   143  func (u *TargetedUI) Message(message string) {
   144  	u.Ui.Message(u.prefixLines(false, message))
   145  }
   146  
   147  func (u *TargetedUI) Error(message string) {
   148  	u.Ui.Error(u.prefixLines(true, message))
   149  }
   150  
   151  func (u *TargetedUI) Machine(t string, args ...string) {
   152  	// Prefix in the target, then pass through
   153  	u.Ui.Machine(fmt.Sprintf("%s,%s", u.Target, t), args...)
   154  }
   155  
   156  func (u *TargetedUI) prefixLines(arrow bool, message string) string {
   157  	arrowText := "==>"
   158  	if !arrow {
   159  		arrowText = strings.Repeat(" ", len(arrowText))
   160  	}
   161  
   162  	var result bytes.Buffer
   163  
   164  	for _, line := range strings.Split(message, "\n") {
   165  		result.WriteString(fmt.Sprintf("%s %s: %s\n", arrowText, u.Target, line))
   166  	}
   167  
   168  	return strings.TrimRightFunc(result.String(), unicode.IsSpace)
   169  }
   170  
   171  func (rw *BasicUi) Ask(query string) (string, error) {
   172  	rw.l.Lock()
   173  	defer rw.l.Unlock()
   174  
   175  	if rw.interrupted {
   176  		return "", errors.New("interrupted")
   177  	}
   178  
   179  	if rw.scanner == nil {
   180  		rw.scanner = bufio.NewScanner(rw.Reader)
   181  	}
   182  	sigCh := make(chan os.Signal, 1)
   183  	signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
   184  	defer signal.Stop(sigCh)
   185  
   186  	log.Printf("ui: ask: %s", query)
   187  	if query != "" {
   188  		if _, err := fmt.Fprint(rw.Writer, query+" "); err != nil {
   189  			return "", err
   190  		}
   191  	}
   192  
   193  	result := make(chan string, 1)
   194  	go func() {
   195  		var line string
   196  		if rw.scanner.Scan() {
   197  			line = rw.scanner.Text()
   198  		}
   199  		if err := rw.scanner.Err(); err != nil {
   200  			log.Printf("ui: scan err: %s", err)
   201  			return
   202  		}
   203  		result <- line
   204  	}()
   205  
   206  	select {
   207  	case line := <-result:
   208  		return line, nil
   209  	case <-sigCh:
   210  		// Print a newline so that any further output starts properly
   211  		// on a new line.
   212  		fmt.Fprintln(rw.Writer)
   213  
   214  		// Mark that we were interrupted so future Ask calls fail.
   215  		rw.interrupted = true
   216  
   217  		return "", errors.New("interrupted")
   218  	}
   219  }
   220  
   221  func (rw *BasicUi) Say(message string) {
   222  	rw.l.Lock()
   223  	defer rw.l.Unlock()
   224  
   225  	log.Printf("ui: %s", message)
   226  	_, err := fmt.Fprint(rw.Writer, message+"\n")
   227  	if err != nil {
   228  		log.Printf("[ERR] Failed to write to UI: %s", err)
   229  	}
   230  }
   231  
   232  func (rw *BasicUi) Message(message string) {
   233  	rw.l.Lock()
   234  	defer rw.l.Unlock()
   235  
   236  	log.Printf("ui: %s", message)
   237  	_, err := fmt.Fprint(rw.Writer, message+"\n")
   238  	if err != nil {
   239  		log.Printf("[ERR] Failed to write to UI: %s", err)
   240  	}
   241  }
   242  
   243  func (rw *BasicUi) Error(message string) {
   244  	rw.l.Lock()
   245  	defer rw.l.Unlock()
   246  
   247  	writer := rw.ErrorWriter
   248  	if writer == nil {
   249  		writer = rw.Writer
   250  	}
   251  
   252  	log.Printf("ui error: %s", message)
   253  	_, err := fmt.Fprint(writer, message+"\n")
   254  	if err != nil {
   255  		log.Printf("[ERR] Failed to write to UI: %s", err)
   256  	}
   257  }
   258  
   259  func (rw *BasicUi) Machine(t string, args ...string) {
   260  	log.Printf("machine readable: %s %#v", t, args)
   261  }
   262  
   263  func (u *MachineReadableUi) Ask(query string) (string, error) {
   264  	return "", errors.New("machine-readable UI can't ask")
   265  }
   266  
   267  func (u *MachineReadableUi) Say(message string) {
   268  	u.Machine("ui", "say", message)
   269  }
   270  
   271  func (u *MachineReadableUi) Message(message string) {
   272  	u.Machine("ui", "message", message)
   273  }
   274  
   275  func (u *MachineReadableUi) Error(message string) {
   276  	u.Machine("ui", "error", message)
   277  }
   278  
   279  func (u *MachineReadableUi) Machine(category string, args ...string) {
   280  	now := time.Now().UTC()
   281  
   282  	// Determine if we have a target, and set it
   283  	target := ""
   284  	commaIdx := strings.Index(category, ",")
   285  	if commaIdx > -1 {
   286  		target = category[0:commaIdx]
   287  		category = category[commaIdx+1:]
   288  	}
   289  
   290  	// Prepare the args
   291  	for i, v := range args {
   292  		args[i] = strings.Replace(v, ",", "%!(PACKER_COMMA)", -1)
   293  		args[i] = strings.Replace(args[i], "\r", "\\r", -1)
   294  		args[i] = strings.Replace(args[i], "\n", "\\n", -1)
   295  	}
   296  	argsString := strings.Join(args, ",")
   297  
   298  	_, err := fmt.Fprintf(u.Writer, "%d,%s,%s,%s\n", now.Unix(), target, category, argsString)
   299  	if err != nil {
   300  		if err == syscall.EPIPE || strings.Contains(err.Error(), "broken pipe") {
   301  			// Ignore epipe errors because that just means that the file
   302  			// is probably closed or going to /dev/null or something.
   303  		} else {
   304  			panic(err)
   305  		}
   306  	}
   307  }