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 }