func (r *Runner) getBuildLog(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { id := ps.ByName("build") b := &Build{} if err := r.db.View(func(tx *bolt.Tx) error { v := tx.Bucket(dbBucket).Get([]byte(id)) if err := json.Unmarshal(v, b); err != nil { return fmt.Errorf("could not decode build %s: %s", v, err) } return nil }); err != nil { http.Error(w, err.Error(), 500) return } if b.Finished() { if b.Version == BuildVersion1 { http.Redirect(w, req, b.LogURL, http.StatusMovedPermanently) return } if strings.Contains(req.Header.Get("Accept"), "text/event-stream") { if err := serveBuildLogStream(b, w); err != nil { http.Error(w, err.Error(), 500) } return } http.ServeFile(w, req, path.Join(args.AssetsDir, "build-log.html")) return } t, err := tail.TailFile(b.LogFile, tail.Config{Follow: true, MustExist: true}) if err != nil { http.Error(w, err.Error(), 500) return } if cn, ok := w.(http.CloseNotifier); ok { go func() { <-cn.CloseNotify() t.Stop() }() } else { defer t.Stop() } flush := func() { if fw, ok := w.(http.Flusher); ok { fw.Flush() } } w.Header().Set("Content-Type", textPlain) w.WriteHeader(http.StatusOK) flush() for line := range t.Lines { if _, err := io.WriteString(w, line.Text+"\n"); err != nil { log.Printf("serveBuildLog write error: %s\n", err) return } flush() if strings.HasPrefix(line.Text, "build finished") { return } } }
func tailFile(filename string, config tail.Config, done chan bool) { defer func() { done <- true }() t, err := tail.TailFile(filename, config) if err != nil { fmt.Println(err) return } for line := range t.Lines { fmt.Println(line.Text) } err = t.Wait() if err != nil { fmt.Println(err) } }
func getBuildLogStream(b *Build, ch chan string) (stream.Stream, error) { stream := stream.New() // if the build hasn't finished, tail the log from disk if !b.Finished() { t, err := tail.TailFile(b.LogFile, tail.Config{Follow: true, MustExist: true}) if err != nil { return nil, err } go func() { defer t.Stop() defer close(ch) for { select { case line, ok := <-t.Lines: if !ok { stream.Error = t.Err() return } select { case ch <- line.Text: case <-stream.StopCh: return } if strings.HasPrefix(line.Text, "build finished") { return } case <-stream.StopCh: return } } }() return stream, nil } // get the multipart log from S3 and serve just the "build.log" file res, err := http.Get(b.LogURL) if err != nil { return nil, err } if res.StatusCode != http.StatusOK { res.Body.Close() return nil, fmt.Errorf("unexpected status %d getting build log", res.StatusCode) } _, params, err := mime.ParseMediaType(res.Header.Get("Content-Type")) if err != nil { res.Body.Close() return nil, err } go func() { defer res.Body.Close() defer close(ch) mr := multipart.NewReader(res.Body, params["boundary"]) for { select { case <-stream.StopCh: return default: } p, err := mr.NextPart() if err != nil { stream.Error = err return } if p.FileName() != "build.log" { continue } s := bufio.NewScanner(p) for s.Scan() { select { case ch <- s.Text(): case <-stream.StopCh: return } } return } }() return stream, nil }
// Read old log lines from a logfile. func (l *Log) Read(lines int, follow bool, ch chan Data, done chan struct{}) error { name := l.l.Filename var seek int64 if lines == 0 { f, err := os.Open(name) defer f.Close() if err != nil { return err } if seek, err = f.Seek(0, os.SEEK_END); err != nil { return err } } else if lines == -1 { // return all lines dir := filepath.Dir(name) files, err := ioutil.ReadDir(dir) if err != nil { return err } basename := filepath.Base(name) ext := filepath.Ext(basename) id := strings.TrimSuffix(basename, ext) for _, f := range files { if !(strings.HasPrefix(f.Name(), id+"-") && strings.HasSuffix(f.Name(), ext)) { continue } t, err := tail.TailFile(filepath.Join(dir, f.Name()), tail.Config{ Logger: tail.DiscardingLogger, }) if err != nil { return err } for line := range t.Lines { data := Data{} if err := json.Unmarshal([]byte(line.Text), &data); err != nil { return err } ch <- data } } } t, err := tail.TailFile(name, tail.Config{ Follow: follow, ReOpen: follow, Logger: tail.DiscardingLogger, Location: &tail.SeekInfo{ Offset: seek, Whence: os.SEEK_SET, }, }) if err != nil { return err } defer t.Stop() closed := l.closed outer: for { select { case line, ok := <-t.Lines: if !ok { break outer } data := Data{} if err := json.Unmarshal([]byte(line.Text), &data); err != nil { return err } ch <- data case <-done: break outer case <-closed: // StopAtEOF will wait until the log hits an EOF before closing // t.Lines go t.StopAtEOF() // make sure that we don't hit this path again closed = nil } } close(ch) // send a close event so we know everything was read return nil }