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 "syscall" 23 "time" 24 25 "github.com/golang/protobuf/proto" 26 27 "android/soong/ui/logger" 28 "android/soong/ui/status/ninja_frontend" 29) 30 31// NewNinjaReader reads the protobuf frontend format from ninja and translates it 32// into calls on the ToolStatus API. 33func NewNinjaReader(ctx logger.Logger, status ToolStatus, fifo string) *NinjaReader { 34 os.Remove(fifo) 35 36 err := syscall.Mkfifo(fifo, 0666) 37 if err != nil { 38 ctx.Fatalf("Failed to mkfifo(%q): %v", fifo, err) 39 } 40 41 n := &NinjaReader{ 42 status: status, 43 fifo: fifo, 44 done: make(chan bool), 45 cancel: make(chan bool), 46 } 47 48 go n.run() 49 50 return n 51} 52 53type NinjaReader struct { 54 status ToolStatus 55 fifo string 56 done chan bool 57 cancel chan bool 58} 59 60const NINJA_READER_CLOSE_TIMEOUT = 5 * time.Second 61 62// Close waits for NinjaReader to finish reading from the fifo, or 5 seconds. 63func (n *NinjaReader) Close() { 64 // Signal the goroutine to stop if it is blocking opening the fifo. 65 close(n.cancel) 66 67 timeoutCh := time.After(NINJA_READER_CLOSE_TIMEOUT) 68 69 select { 70 case <-n.done: 71 // Nothing 72 case <-timeoutCh: 73 n.status.Error(fmt.Sprintf("ninja fifo didn't finish after %s", NINJA_READER_CLOSE_TIMEOUT.String())) 74 } 75 76 return 77} 78 79func (n *NinjaReader) run() { 80 defer close(n.done) 81 82 // Opening the fifo can block forever if ninja never opens the write end, do it in a goroutine so this 83 // method can exit on cancel. 84 fileCh := make(chan *os.File) 85 go func() { 86 f, err := os.Open(n.fifo) 87 if err != nil { 88 n.status.Error(fmt.Sprintf("Failed to open fifo: %v", err)) 89 close(fileCh) 90 return 91 } 92 fileCh <- f 93 }() 94 95 var f *os.File 96 97 select { 98 case f = <-fileCh: 99 // Nothing 100 case <-n.cancel: 101 return 102 } 103 104 defer f.Close() 105 106 r := bufio.NewReader(f) 107 108 running := map[uint32]*Action{} 109 110 for { 111 size, err := readVarInt(r) 112 if err != nil { 113 if err != io.EOF { 114 n.status.Error(fmt.Sprintf("Got error reading from ninja: %s", err)) 115 } 116 return 117 } 118 119 buf := make([]byte, size) 120 _, err = io.ReadFull(r, buf) 121 if err != nil { 122 if err == io.EOF { 123 n.status.Print(fmt.Sprintf("Missing message of size %d from ninja\n", size)) 124 } else { 125 n.status.Error(fmt.Sprintf("Got error reading from ninja: %s", err)) 126 } 127 return 128 } 129 130 msg := &ninja_frontend.Status{} 131 err = proto.Unmarshal(buf, msg) 132 if err != nil { 133 n.status.Print(fmt.Sprintf("Error reading message from ninja: %v", err)) 134 continue 135 } 136 137 // Ignore msg.BuildStarted 138 if msg.TotalEdges != nil { 139 n.status.SetTotalActions(int(msg.TotalEdges.GetTotalEdges())) 140 } 141 if msg.EdgeStarted != nil { 142 action := &Action{ 143 Description: msg.EdgeStarted.GetDesc(), 144 Outputs: msg.EdgeStarted.Outputs, 145 Command: msg.EdgeStarted.GetCommand(), 146 } 147 n.status.StartAction(action) 148 running[msg.EdgeStarted.GetId()] = action 149 } 150 if msg.EdgeFinished != nil { 151 if started, ok := running[msg.EdgeFinished.GetId()]; ok { 152 delete(running, msg.EdgeFinished.GetId()) 153 154 var err error 155 exitCode := int(msg.EdgeFinished.GetStatus()) 156 if exitCode != 0 { 157 err = fmt.Errorf("exited with code: %d", exitCode) 158 } 159 160 n.status.FinishAction(ActionResult{ 161 Action: started, 162 Output: msg.EdgeFinished.GetOutput(), 163 Error: err, 164 }) 165 } 166 } 167 if msg.Message != nil { 168 message := "ninja: " + msg.Message.GetMessage() 169 switch msg.Message.GetLevel() { 170 case ninja_frontend.Status_Message_INFO: 171 n.status.Status(message) 172 case ninja_frontend.Status_Message_WARNING: 173 n.status.Print("warning: " + message) 174 case ninja_frontend.Status_Message_ERROR: 175 n.status.Error(message) 176 default: 177 n.status.Print(message) 178 } 179 } 180 if msg.BuildFinished != nil { 181 n.status.Finish() 182 } 183 } 184} 185 186func readVarInt(r *bufio.Reader) (int, error) { 187 ret := 0 188 shift := uint(0) 189 190 for { 191 b, err := r.ReadByte() 192 if err != nil { 193 return 0, err 194 } 195 196 ret += int(b&0x7f) << (shift * 7) 197 if b&0x80 == 0 { 198 break 199 } 200 shift += 1 201 if shift > 4 { 202 return 0, fmt.Errorf("Expected varint32 length-delimited message") 203 } 204 } 205 206 return ret, nil 207} 208