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 "runtime" 24 "strings" 25 "syscall" 26 "time" 27 28 "google.golang.org/protobuf/proto" 29 30 "android/soong/ui/logger" 31 "android/soong/ui/status/ninja_frontend" 32) 33 34// NewNinjaReader reads the protobuf frontend format from ninja and translates it 35// into calls on the ToolStatus API. 36func NewNinjaReader(ctx logger.Logger, status ToolStatus, fifo string) *NinjaReader { 37 os.Remove(fifo) 38 39 if err := syscall.Mkfifo(fifo, 0666); err != nil { 40 ctx.Fatalf("Failed to mkfifo(%q): %v", fifo, err) 41 } 42 43 n := &NinjaReader{ 44 status: status, 45 fifo: fifo, 46 forceClose: make(chan bool), 47 done: make(chan bool), 48 cancelOpen: make(chan bool), 49 running: make(map[uint32]*Action), 50 } 51 52 go n.run() 53 54 return n 55} 56 57type NinjaReader struct { 58 status ToolStatus 59 fifo string 60 forceClose chan bool 61 done chan bool 62 cancelOpen chan bool 63 running map[uint32]*Action 64} 65 66const NINJA_READER_CLOSE_TIMEOUT = 5 * time.Second 67 68// Close waits for NinjaReader to finish reading from the fifo, or 5 seconds. 69func (n *NinjaReader) Close() { 70 // Signal the goroutine to stop if it is blocking opening the fifo. 71 close(n.cancelOpen) 72 73 closed := false 74 75 // Ninja should already have exited or been killed, wait 5 seconds for the FIFO to be closed and any 76 // remaining messages to be processed through the NinjaReader.run goroutine. 77 timeoutCh := time.After(NINJA_READER_CLOSE_TIMEOUT) 78 select { 79 case <-n.done: 80 closed = true 81 case <-timeoutCh: 82 // Channel is not closed yet 83 } 84 85 if !closed { 86 n.status.Error(fmt.Sprintf("ninja fifo didn't finish after %s", NINJA_READER_CLOSE_TIMEOUT.String())) 87 88 // Force close the reader even if the FIFO didn't close. 89 close(n.forceClose) 90 91 // Wait again for the reader thread to acknowledge the close before giving up and assuming it isn't going 92 // to send anything else. 93 timeoutCh = time.After(NINJA_READER_CLOSE_TIMEOUT) 94 select { 95 case <-n.done: 96 closed = true 97 case <-timeoutCh: 98 // Channel is not closed yet 99 } 100 } 101 102 if !closed { 103 n.status.Verbose(fmt.Sprintf("ninja fifo didn't finish even after force closing after %s", NINJA_READER_CLOSE_TIMEOUT.String())) 104 } 105 106 err := fmt.Errorf("error: action cancelled when ninja exited") 107 for _, action := range n.running { 108 n.status.FinishAction(ActionResult{ 109 Action: action, 110 Output: err.Error(), 111 Error: err, 112 }) 113 } 114} 115 116func (n *NinjaReader) run() { 117 defer close(n.done) 118 119 // Opening the fifo can block forever if ninja never opens the write end, do it in a goroutine so this 120 // method can exit on cancel. 121 fileCh := make(chan *os.File) 122 go func() { 123 f, err := os.Open(n.fifo) 124 if err != nil { 125 n.status.Error(fmt.Sprintf("Failed to open fifo: %v", err)) 126 close(fileCh) 127 return 128 } 129 fileCh <- f 130 }() 131 132 var f *os.File 133 134 select { 135 case f = <-fileCh: 136 // Nothing 137 case <-n.cancelOpen: 138 return 139 } 140 141 defer f.Close() 142 143 r := bufio.NewReader(f) 144 145 msgChan := make(chan *ninja_frontend.Status) 146 147 // Read from the ninja fifo and decode the protobuf in a goroutine so the main NinjaReader.run goroutine 148 // can listen 149 go func() { 150 defer close(msgChan) 151 for { 152 size, err := readVarInt(r) 153 if err != nil { 154 if err != io.EOF { 155 n.status.Error(fmt.Sprintf("Got error reading from ninja: %s", err)) 156 } 157 return 158 } 159 160 buf := make([]byte, size) 161 _, err = io.ReadFull(r, buf) 162 if err != nil { 163 if err == io.EOF { 164 n.status.Print(fmt.Sprintf("Missing message of size %d from ninja\n", size)) 165 } else { 166 n.status.Error(fmt.Sprintf("Got error reading from ninja: %s", err)) 167 } 168 return 169 } 170 171 msg := &ninja_frontend.Status{} 172 err = proto.Unmarshal(buf, msg) 173 if err != nil { 174 n.status.Print(fmt.Sprintf("Error reading message from ninja: %v", err)) 175 continue 176 } 177 178 msgChan <- msg 179 } 180 }() 181 182 for { 183 var msg *ninja_frontend.Status 184 var msgOk bool 185 select { 186 case <-n.forceClose: 187 // Close() has been called, but the reader goroutine didn't get EOF after 5 seconds 188 break 189 case msg, msgOk = <-msgChan: 190 // msg is ready or closed 191 } 192 193 if !msgOk { 194 // msgChan is closed 195 break 196 } 197 198 if msg.BuildStarted != nil { 199 parallelism := uint32(runtime.NumCPU()) 200 if msg.BuildStarted.GetParallelism() > 0 { 201 parallelism = msg.BuildStarted.GetParallelism() 202 } 203 // It is estimated from total time / parallelism assumming the build is packing enough. 204 estimatedDurationFromTotal := time.Duration(msg.BuildStarted.GetEstimatedTotalTime()/parallelism) * time.Millisecond 205 // It is estimated from critical path time which is useful for small size build. 206 estimatedDurationFromCriticalPath := time.Duration(msg.BuildStarted.GetCriticalPathTime()) * time.Millisecond 207 // Select the longer one. 208 estimatedDuration := max(estimatedDurationFromTotal, estimatedDurationFromCriticalPath) 209 210 if estimatedDuration > 0 { 211 n.status.SetEstimatedTime(time.Now().Add(estimatedDuration)) 212 n.status.Verbose(fmt.Sprintf("parallelism: %d, estimated from total time: %s, critical path time: %s", 213 parallelism, 214 estimatedDurationFromTotal, 215 estimatedDurationFromCriticalPath)) 216 217 } 218 } 219 if msg.TotalEdges != nil { 220 n.status.SetTotalActions(int(msg.TotalEdges.GetTotalEdges())) 221 } 222 if msg.EdgeStarted != nil { 223 action := &Action{ 224 Description: msg.EdgeStarted.GetDesc(), 225 Outputs: msg.EdgeStarted.Outputs, 226 Inputs: msg.EdgeStarted.Inputs, 227 Command: msg.EdgeStarted.GetCommand(), 228 ChangedInputs: msg.EdgeStarted.ChangedInputs, 229 } 230 n.status.StartAction(action) 231 n.running[msg.EdgeStarted.GetId()] = action 232 } 233 if msg.EdgeFinished != nil { 234 if started, ok := n.running[msg.EdgeFinished.GetId()]; ok { 235 delete(n.running, msg.EdgeFinished.GetId()) 236 237 var err error 238 exitCode := int(msg.EdgeFinished.GetStatus()) 239 if exitCode != 0 { 240 err = fmt.Errorf("exited with code: %d", exitCode) 241 } 242 243 outputWithErrorHint := errorHintGenerator.GetOutputWithErrorHint(msg.EdgeFinished.GetOutput(), exitCode) 244 n.status.FinishAction(ActionResult{ 245 Action: started, 246 Output: outputWithErrorHint, 247 Error: err, 248 Stats: ActionResultStats{ 249 UserTime: msg.EdgeFinished.GetUserTime(), 250 SystemTime: msg.EdgeFinished.GetSystemTime(), 251 MaxRssKB: msg.EdgeFinished.GetMaxRssKb(), 252 MinorPageFaults: msg.EdgeFinished.GetMinorPageFaults(), 253 MajorPageFaults: msg.EdgeFinished.GetMajorPageFaults(), 254 IOInputKB: msg.EdgeFinished.GetIoInputKb(), 255 IOOutputKB: msg.EdgeFinished.GetIoOutputKb(), 256 VoluntaryContextSwitches: msg.EdgeFinished.GetVoluntaryContextSwitches(), 257 InvoluntaryContextSwitches: msg.EdgeFinished.GetInvoluntaryContextSwitches(), 258 Tags: msg.EdgeFinished.GetTags(), 259 }, 260 }) 261 } 262 } 263 if msg.Message != nil { 264 message := "ninja: " + msg.Message.GetMessage() 265 switch msg.Message.GetLevel() { 266 case ninja_frontend.Status_Message_INFO: 267 n.status.Status(message) 268 case ninja_frontend.Status_Message_WARNING: 269 n.status.Print("warning: " + message) 270 case ninja_frontend.Status_Message_ERROR: 271 n.status.Error(message) 272 case ninja_frontend.Status_Message_DEBUG: 273 n.status.Verbose(message) 274 default: 275 n.status.Print(message) 276 } 277 } 278 if msg.BuildFinished != nil { 279 n.status.Finish() 280 } 281 } 282} 283 284func readVarInt(r *bufio.Reader) (int, error) { 285 ret := 0 286 shift := uint(0) 287 288 for { 289 b, err := r.ReadByte() 290 if err != nil { 291 return 0, err 292 } 293 294 ret += int(b&0x7f) << (shift * 7) 295 if b&0x80 == 0 { 296 break 297 } 298 shift += 1 299 if shift > 4 { 300 return 0, fmt.Errorf("Expected varint32 length-delimited message") 301 } 302 } 303 304 return ret, nil 305} 306 307// key is pattern in stdout/stderr 308// value is error hint 309var allErrorHints = map[string]string{ 310 "Read-only file system": `\nWrite to a read-only file system detected. Possible fixes include 3111. Generate file directly to out/ which is ReadWrite, #recommend solution 3122. BUILD_BROKEN_SRC_DIR_RW_ALLOWLIST := <my/path/1> <my/path/2> #discouraged, subset of source tree will be RW 3133. BUILD_BROKEN_SRC_DIR_IS_WRITABLE := true #highly discouraged, entire source tree will be RW 314`, 315} 316var errorHintGenerator = *newErrorHintGenerator(allErrorHints) 317 318type ErrorHintGenerator struct { 319 allErrorHints map[string]string 320 allErrorHintPatternsCompiled *regexp.Regexp 321} 322 323func newErrorHintGenerator(allErrorHints map[string]string) *ErrorHintGenerator { 324 var allErrorHintPatterns []string 325 for errorHintPattern, _ := range allErrorHints { 326 allErrorHintPatterns = append(allErrorHintPatterns, errorHintPattern) 327 } 328 allErrorHintPatternsRegex := strings.Join(allErrorHintPatterns[:], "|") 329 re := regexp.MustCompile(allErrorHintPatternsRegex) 330 return &ErrorHintGenerator{ 331 allErrorHints: allErrorHints, 332 allErrorHintPatternsCompiled: re, 333 } 334} 335 336func (errorHintGenerator *ErrorHintGenerator) GetOutputWithErrorHint(rawOutput string, buildExitCode int) string { 337 if buildExitCode == 0 { 338 return rawOutput 339 } 340 errorHint := errorHintGenerator.getErrorHint(rawOutput) 341 if errorHint == nil { 342 return rawOutput 343 } 344 return rawOutput + *errorHint 345} 346 347// Returns the error hint corresponding to the FIRST match in raw output 348func (errorHintGenerator *ErrorHintGenerator) getErrorHint(rawOutput string) *string { 349 firstMatch := errorHintGenerator.allErrorHintPatternsCompiled.FindString(rawOutput) 350 if _, found := errorHintGenerator.allErrorHints[firstMatch]; found { 351 errorHint := errorHintGenerator.allErrorHints[firstMatch] 352 return &errorHint 353 } 354 return nil 355} 356