1// Copyright 2018 Google Inc. All rights reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package status 16 17import ( 18 "bufio" 19 "fmt" 20 "io" 21 "os" 22 "regexp" 23 "strings" 24 "syscall" 25 "time" 26 27 "google.golang.org/protobuf/proto" 28 29 "android/soong/ui/logger" 30 "android/soong/ui/status/ninja_frontend" 31) 32 33// NewNinjaReader reads the protobuf frontend format from ninja and translates it 34// into calls on the ToolStatus API. 35func NewNinjaReader(ctx logger.Logger, status ToolStatus, fifo string) *NinjaReader { 36 os.Remove(fifo) 37 38 if err := syscall.Mkfifo(fifo, 0666); err != nil { 39 ctx.Fatalf("Failed to mkfifo(%q): %v", fifo, err) 40 } 41 42 n := &NinjaReader{ 43 status: status, 44 fifo: fifo, 45 done: make(chan bool), 46 cancel: make(chan bool), 47 } 48 49 go n.run() 50 51 return n 52} 53 54type NinjaReader struct { 55 status ToolStatus 56 fifo string 57 done chan bool 58 cancel chan bool 59} 60 61const NINJA_READER_CLOSE_TIMEOUT = 5 * time.Second 62 63// Close waits for NinjaReader to finish reading from the fifo, or 5 seconds. 64func (n *NinjaReader) Close() { 65 // Signal the goroutine to stop if it is blocking opening the fifo. 66 close(n.cancel) 67 68 timeoutCh := time.After(NINJA_READER_CLOSE_TIMEOUT) 69 70 select { 71 case <-n.done: 72 // Nothing 73 case <-timeoutCh: 74 n.status.Error(fmt.Sprintf("ninja fifo didn't finish after %s", NINJA_READER_CLOSE_TIMEOUT.String())) 75 } 76 77 return 78} 79 80func (n *NinjaReader) run() { 81 defer close(n.done) 82 83 // Opening the fifo can block forever if ninja never opens the write end, do it in a goroutine so this 84 // method can exit on cancel. 85 fileCh := make(chan *os.File) 86 go func() { 87 f, err := os.Open(n.fifo) 88 if err != nil { 89 n.status.Error(fmt.Sprintf("Failed to open fifo: %v", err)) 90 close(fileCh) 91 return 92 } 93 fileCh <- f 94 }() 95 96 var f *os.File 97 98 select { 99 case f = <-fileCh: 100 // Nothing 101 case <-n.cancel: 102 return 103 } 104 105 defer f.Close() 106 107 r := bufio.NewReader(f) 108 109 running := map[uint32]*Action{} 110 111 for { 112 size, err := readVarInt(r) 113 if err != nil { 114 if err != io.EOF { 115 n.status.Error(fmt.Sprintf("Got error reading from ninja: %s", err)) 116 } 117 return 118 } 119 120 buf := make([]byte, size) 121 _, err = io.ReadFull(r, buf) 122 if err != nil { 123 if err == io.EOF { 124 n.status.Print(fmt.Sprintf("Missing message of size %d from ninja\n", size)) 125 } else { 126 n.status.Error(fmt.Sprintf("Got error reading from ninja: %s", err)) 127 } 128 return 129 } 130 131 msg := &ninja_frontend.Status{} 132 err = proto.Unmarshal(buf, msg) 133 if err != nil { 134 n.status.Print(fmt.Sprintf("Error reading message from ninja: %v", err)) 135 continue 136 } 137 138 // Ignore msg.BuildStarted 139 if msg.TotalEdges != nil { 140 n.status.SetTotalActions(int(msg.TotalEdges.GetTotalEdges())) 141 } 142 if msg.EdgeStarted != nil { 143 action := &Action{ 144 Description: msg.EdgeStarted.GetDesc(), 145 Outputs: msg.EdgeStarted.Outputs, 146 Inputs: msg.EdgeStarted.Inputs, 147 Command: msg.EdgeStarted.GetCommand(), 148 } 149 n.status.StartAction(action) 150 running[msg.EdgeStarted.GetId()] = action 151 } 152 if msg.EdgeFinished != nil { 153 if started, ok := running[msg.EdgeFinished.GetId()]; ok { 154 delete(running, msg.EdgeFinished.GetId()) 155 156 var err error 157 exitCode := int(msg.EdgeFinished.GetStatus()) 158 if exitCode != 0 { 159 err = fmt.Errorf("exited with code: %d", exitCode) 160 } 161 162 outputWithErrorHint := errorHintGenerator.GetOutputWithErrorHint(msg.EdgeFinished.GetOutput(), exitCode) 163 n.status.FinishAction(ActionResult{ 164 Action: started, 165 Output: outputWithErrorHint, 166 Error: err, 167 Stats: ActionResultStats{ 168 UserTime: msg.EdgeFinished.GetUserTime(), 169 SystemTime: msg.EdgeFinished.GetSystemTime(), 170 MaxRssKB: msg.EdgeFinished.GetMaxRssKb(), 171 MinorPageFaults: msg.EdgeFinished.GetMinorPageFaults(), 172 MajorPageFaults: msg.EdgeFinished.GetMajorPageFaults(), 173 IOInputKB: msg.EdgeFinished.GetIoInputKb(), 174 IOOutputKB: msg.EdgeFinished.GetIoOutputKb(), 175 VoluntaryContextSwitches: msg.EdgeFinished.GetVoluntaryContextSwitches(), 176 InvoluntaryContextSwitches: msg.EdgeFinished.GetInvoluntaryContextSwitches(), 177 }, 178 }) 179 } 180 } 181 if msg.Message != nil { 182 message := "ninja: " + msg.Message.GetMessage() 183 switch msg.Message.GetLevel() { 184 case ninja_frontend.Status_Message_INFO: 185 n.status.Status(message) 186 case ninja_frontend.Status_Message_WARNING: 187 n.status.Print("warning: " + message) 188 case ninja_frontend.Status_Message_ERROR: 189 n.status.Error(message) 190 case ninja_frontend.Status_Message_DEBUG: 191 n.status.Verbose(message) 192 default: 193 n.status.Print(message) 194 } 195 } 196 if msg.BuildFinished != nil { 197 n.status.Finish() 198 } 199 } 200} 201 202func readVarInt(r *bufio.Reader) (int, error) { 203 ret := 0 204 shift := uint(0) 205 206 for { 207 b, err := r.ReadByte() 208 if err != nil { 209 return 0, err 210 } 211 212 ret += int(b&0x7f) << (shift * 7) 213 if b&0x80 == 0 { 214 break 215 } 216 shift += 1 217 if shift > 4 { 218 return 0, fmt.Errorf("Expected varint32 length-delimited message") 219 } 220 } 221 222 return ret, nil 223} 224 225// key is pattern in stdout/stderr 226// value is error hint 227var allErrorHints = map[string]string{ 228 "Read-only file system": `\nWrite to a read-only file system detected. Possible fixes include 2291. Generate file directly to out/ which is ReadWrite, #recommend solution 2302. BUILD_BROKEN_SRC_DIR_RW_ALLOWLIST := <my/path/1> <my/path/2> #discouraged, subset of source tree will be RW 2313. BUILD_BROKEN_SRC_DIR_IS_WRITABLE := true #highly discouraged, entire source tree will be RW 232`, 233} 234var errorHintGenerator = *newErrorHintGenerator(allErrorHints) 235 236type ErrorHintGenerator struct { 237 allErrorHints map[string]string 238 allErrorHintPatternsCompiled *regexp.Regexp 239} 240 241func newErrorHintGenerator(allErrorHints map[string]string) *ErrorHintGenerator { 242 var allErrorHintPatterns []string 243 for errorHintPattern, _ := range allErrorHints { 244 allErrorHintPatterns = append(allErrorHintPatterns, errorHintPattern) 245 } 246 allErrorHintPatternsRegex := strings.Join(allErrorHintPatterns[:], "|") 247 re := regexp.MustCompile(allErrorHintPatternsRegex) 248 return &ErrorHintGenerator{ 249 allErrorHints: allErrorHints, 250 allErrorHintPatternsCompiled: re, 251 } 252} 253 254func (errorHintGenerator *ErrorHintGenerator) GetOutputWithErrorHint(rawOutput string, buildExitCode int) string { 255 if buildExitCode == 0 { 256 return rawOutput 257 } 258 errorHint := errorHintGenerator.getErrorHint(rawOutput) 259 if errorHint == nil { 260 return rawOutput 261 } 262 return rawOutput + *errorHint 263} 264 265// Returns the error hint corresponding to the FIRST match in raw output 266func (errorHintGenerator *ErrorHintGenerator) getErrorHint(rawOutput string) *string { 267 firstMatch := errorHintGenerator.allErrorHintPatternsCompiled.FindString(rawOutput) 268 if _, found := errorHintGenerator.allErrorHints[firstMatch]; found { 269 errorHint := errorHintGenerator.allErrorHints[firstMatch] 270 return &errorHint 271 } 272 return nil 273} 274