github.com/askholme/packer@v0.7.2-0.20140924152349-70d9566a6852/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 }