• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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