github.com/dacamp/packer@v0.10.2/packer/ui.go (about)

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