blob: 73edbf62522c71d0b14b0ad96f72f374246b48d9 [file] [log] [blame]
Dan Willemsenb82471a2018-05-17 16:37:09 -07001// 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"
Spandan Das05063612021-06-25 01:39:04 +000022 "regexp"
Jeongik Cha3622b342023-11-27 11:00:52 +090023 "runtime"
Spandan Das05063612021-06-25 01:39:04 +000024 "strings"
Dan Willemsenb82471a2018-05-17 16:37:09 -070025 "syscall"
Colin Crossb98d3bc2019-03-21 16:02:58 -070026 "time"
Dan Willemsenb82471a2018-05-17 16:37:09 -070027
Dan Willemsen4591b642021-05-24 14:24:12 -070028 "google.golang.org/protobuf/proto"
Dan Willemsenb82471a2018-05-17 16:37:09 -070029
30 "android/soong/ui/logger"
31 "android/soong/ui/status/ninja_frontend"
32)
33
Colin Crossb98d3bc2019-03-21 16:02:58 -070034// NewNinjaReader reads the protobuf frontend format from ninja and translates it
Dan Willemsenb82471a2018-05-17 16:37:09 -070035// into calls on the ToolStatus API.
Colin Crossb98d3bc2019-03-21 16:02:58 -070036func NewNinjaReader(ctx logger.Logger, status ToolStatus, fifo string) *NinjaReader {
Dan Willemsenb82471a2018-05-17 16:37:09 -070037 os.Remove(fifo)
38
ustafb67fd12022-08-19 19:26:00 -040039 if err := syscall.Mkfifo(fifo, 0666); err != nil {
Dan Willemsenb82471a2018-05-17 16:37:09 -070040 ctx.Fatalf("Failed to mkfifo(%q): %v", fifo, err)
41 }
42
Colin Crossb98d3bc2019-03-21 16:02:58 -070043 n := &NinjaReader{
Colin Crossb0b369c2023-08-30 17:32:17 -070044 status: status,
45 fifo: fifo,
46 forceClose: make(chan bool),
47 done: make(chan bool),
48 cancelOpen: make(chan bool),
Colin Crosseb77e102025-02-13 12:49:02 -080049 running: make(map[uint32]*Action),
Colin Crossb98d3bc2019-03-21 16:02:58 -070050 }
51
52 go n.run()
53
54 return n
Dan Willemsenb82471a2018-05-17 16:37:09 -070055}
56
Colin Crossb98d3bc2019-03-21 16:02:58 -070057type NinjaReader struct {
Colin Crossb0b369c2023-08-30 17:32:17 -070058 status ToolStatus
59 fifo string
60 forceClose chan bool
61 done chan bool
62 cancelOpen chan bool
Colin Crosseb77e102025-02-13 12:49:02 -080063 running map[uint32]*Action
Colin Crossb98d3bc2019-03-21 16:02:58 -070064}
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.
Colin Crossb0b369c2023-08-30 17:32:17 -070071 close(n.cancelOpen)
Colin Crossb98d3bc2019-03-21 16:02:58 -070072
Colin Crosseb77e102025-02-13 12:49:02 -080073 closed := false
74
Colin Crossb0b369c2023-08-30 17:32:17 -070075 // 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.
Colin Crossb98d3bc2019-03-21 16:02:58 -070077 timeoutCh := time.After(NINJA_READER_CLOSE_TIMEOUT)
Colin Crossb98d3bc2019-03-21 16:02:58 -070078 select {
79 case <-n.done:
Colin Crosseb77e102025-02-13 12:49:02 -080080 closed = true
Colin Crossb98d3bc2019-03-21 16:02:58 -070081 case <-timeoutCh:
Colin Crossb0b369c2023-08-30 17:32:17 -070082 // Channel is not closed yet
Dan Willemsenb82471a2018-05-17 16:37:09 -070083 }
Colin Crossb98d3bc2019-03-21 16:02:58 -070084
Colin Crosseb77e102025-02-13 12:49:02 -080085 if !closed {
86 n.status.Error(fmt.Sprintf("ninja fifo didn't finish after %s", NINJA_READER_CLOSE_TIMEOUT.String()))
Colin Crossb0b369c2023-08-30 17:32:17 -070087
Colin Crosseb77e102025-02-13 12:49:02 -080088 // Force close the reader even if the FIFO didn't close.
89 close(n.forceClose)
Colin Crossb0b369c2023-08-30 17:32:17 -070090
Colin Crosseb77e102025-02-13 12:49:02 -080091 // 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 }
Colin Crossb0b369c2023-08-30 17:32:17 -0700100 }
101
Colin Crosseb77e102025-02-13 12:49:02 -0800102 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 }
Colin Crossb98d3bc2019-03-21 16:02:58 -0700114}
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
Colin Crossb0b369c2023-08-30 17:32:17 -0700137 case <-n.cancelOpen:
Colin Crossb98d3bc2019-03-21 16:02:58 -0700138 return
139 }
140
Dan Willemsenb82471a2018-05-17 16:37:09 -0700141 defer f.Close()
142
143 r := bufio.NewReader(f)
144
Colin Crossb0b369c2023-08-30 17:32:17 -0700145 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
Dan Willemsenb82471a2018-05-17 16:37:09 -0700182 for {
Colin Crossb0b369c2023-08-30 17:32:17 -0700183 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
Dan Willemsenb82471a2018-05-17 16:37:09 -0700191 }
192
Colin Crossb0b369c2023-08-30 17:32:17 -0700193 if !msgOk {
194 // msgChan is closed
195 break
Dan Willemsenb82471a2018-05-17 16:37:09 -0700196 }
Jeongik Cha3622b342023-11-27 11:00:52 +0900197
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))
Jeongik Chad74c9142023-12-11 11:40:51 +0000212 n.status.Verbose(fmt.Sprintf("parallelism: %d, estimated from total time: %s, critical path time: %s",
Jeongik Cha3622b342023-11-27 11:00:52 +0900213 parallelism,
214 estimatedDurationFromTotal,
215 estimatedDurationFromCriticalPath))
216
217 }
218 }
Dan Willemsenb82471a2018-05-17 16:37:09 -0700219 if msg.TotalEdges != nil {
Colin Crossb98d3bc2019-03-21 16:02:58 -0700220 n.status.SetTotalActions(int(msg.TotalEdges.GetTotalEdges()))
Dan Willemsenb82471a2018-05-17 16:37:09 -0700221 }
222 if msg.EdgeStarted != nil {
223 action := &Action{
Yu Liua58467a2024-03-05 01:12:19 +0000224 Description: msg.EdgeStarted.GetDesc(),
225 Outputs: msg.EdgeStarted.Outputs,
226 Inputs: msg.EdgeStarted.Inputs,
227 Command: msg.EdgeStarted.GetCommand(),
228 ChangedInputs: msg.EdgeStarted.ChangedInputs,
Dan Willemsenb82471a2018-05-17 16:37:09 -0700229 }
Colin Crossb98d3bc2019-03-21 16:02:58 -0700230 n.status.StartAction(action)
Colin Crosseb77e102025-02-13 12:49:02 -0800231 n.running[msg.EdgeStarted.GetId()] = action
Dan Willemsenb82471a2018-05-17 16:37:09 -0700232 }
233 if msg.EdgeFinished != nil {
Colin Crosseb77e102025-02-13 12:49:02 -0800234 if started, ok := n.running[msg.EdgeFinished.GetId()]; ok {
235 delete(n.running, msg.EdgeFinished.GetId())
Dan Willemsenb82471a2018-05-17 16:37:09 -0700236
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
Spandan Das05063612021-06-25 01:39:04 +0000243 outputWithErrorHint := errorHintGenerator.GetOutputWithErrorHint(msg.EdgeFinished.GetOutput(), exitCode)
Colin Crossb98d3bc2019-03-21 16:02:58 -0700244 n.status.FinishAction(ActionResult{
Dan Willemsenb82471a2018-05-17 16:37:09 -0700245 Action: started,
Spandan Das05063612021-06-25 01:39:04 +0000246 Output: outputWithErrorHint,
Dan Willemsenb82471a2018-05-17 16:37:09 -0700247 Error: err,
Colin Crossd888b6b2020-10-15 13:46:32 -0700248 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(),
Dan Alberte82234e2023-06-01 23:09:38 +0000258 Tags: msg.EdgeFinished.GetTags(),
Colin Crossd888b6b2020-10-15 13:46:32 -0700259 },
Dan Willemsenb82471a2018-05-17 16:37:09 -0700260 })
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:
Colin Crossb98d3bc2019-03-21 16:02:58 -0700267 n.status.Status(message)
Dan Willemsenb82471a2018-05-17 16:37:09 -0700268 case ninja_frontend.Status_Message_WARNING:
Colin Crossb98d3bc2019-03-21 16:02:58 -0700269 n.status.Print("warning: " + message)
Dan Willemsenb82471a2018-05-17 16:37:09 -0700270 case ninja_frontend.Status_Message_ERROR:
Colin Crossb98d3bc2019-03-21 16:02:58 -0700271 n.status.Error(message)
Dan Willemsen08218222020-05-18 14:02:02 -0700272 case ninja_frontend.Status_Message_DEBUG:
273 n.status.Verbose(message)
Dan Willemsenb82471a2018-05-17 16:37:09 -0700274 default:
Colin Crossb98d3bc2019-03-21 16:02:58 -0700275 n.status.Print(message)
Dan Willemsenb82471a2018-05-17 16:37:09 -0700276 }
277 }
278 if msg.BuildFinished != nil {
Colin Crossb98d3bc2019-03-21 16:02:58 -0700279 n.status.Finish()
Dan Willemsenb82471a2018-05-17 16:37:09 -0700280 }
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}
Spandan Das05063612021-06-25 01:39:04 +0000306
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}