go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/logdog/client/butler/callback_text_wrapper.go (about)

     1  // Copyright 2018 The LUCI Authors.
     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  
    15  package butler
    16  
    17  import (
    18  	"go.chromium.org/luci/common/errors"
    19  	"go.chromium.org/luci/logdog/api/logpb"
    20  )
    21  
    22  // assertGetText panics if the passed LogEntry does not contain Text data, or returns it.
    23  func assertGetText(le *logpb.LogEntry) *logpb.Text {
    24  	if txt := le.GetText(); txt == nil {
    25  		panic(errors.Reason(
    26  			"wrong StreamType: got %T, expected *logpb.LogEntry_Text", le.Content,
    27  		).Err())
    28  	} else {
    29  		return txt
    30  	}
    31  }
    32  
    33  // getWrappedTextCallback wraps a passed callback meant to be called at the
    34  // ends of Text lines so that it is actually called at the end of Text lines.
    35  //
    36  // Does not wrap callback to guarantee being called at the end of *every* Text
    37  // line.
    38  //
    39  // The wrapped callback panics if:
    40  // - the passed LogEntry is not a Text LogEntry
    41  // - the passed LogEntry has lines in a form other than described in log.proto
    42  func getWrappedTextCallback(cb StreamChunkCallback) StreamChunkCallback {
    43  	if cb == nil {
    44  		return nil
    45  	}
    46  
    47  	var flushed bool
    48  	var buf []*logpb.Text_Line
    49  	var streamIdx uint64
    50  	var sequence uint64
    51  
    52  	flushBuffer := func() {
    53  		if len(buf) == 0 {
    54  			return
    55  		}
    56  
    57  		data := &logpb.LogEntry{
    58  			Content: &logpb.LogEntry_Text{
    59  				Text: &logpb.Text{
    60  					Lines: buf,
    61  				},
    62  			},
    63  			StreamIndex: streamIdx,
    64  			Sequence:    sequence,
    65  		}
    66  
    67  		cb(data)
    68  
    69  		streamIdx++
    70  		sequence += uint64(len(buf))
    71  		buf = nil
    72  	}
    73  
    74  	return func(le *logpb.LogEntry) {
    75  		if le == nil && !flushed { // "flush"
    76  			flushed = true
    77  			flushBuffer()
    78  			cb(nil)
    79  			return
    80  		}
    81  		if flushed {
    82  			panic(errors.New("called with nil multiple times"))
    83  		}
    84  
    85  		txt := assertGetText(le)
    86  
    87  		if len(txt.Lines) == 0 {
    88  			panic(errors.New("called with no lines"))
    89  		}
    90  
    91  		// Process the first line, which may be partial.
    92  		firstLine := txt.Lines[0]
    93  		buf = append(buf, firstLine)
    94  
    95  		if firstLine.Delimiter == "" {
    96  			if len(txt.Lines) > 1 {
    97  				panic(errors.New("partial line not last in LogEntry"))
    98  			}
    99  			return
   100  		}
   101  
   102  		// Convert buf's contents into a single line and store that.
   103  		if len(buf) > 1 {
   104  			bufSize := 0
   105  			for _, line := range buf {
   106  				bufSize += len(line.Value)
   107  			}
   108  			wholeFirstLine := &logpb.Text_Line{
   109  				Value:     make([]byte, 0, bufSize),
   110  				Delimiter: firstLine.Delimiter,
   111  			}
   112  			for _, line := range buf {
   113  				wholeFirstLine.Value = append(wholeFirstLine.Value, line.Value...)
   114  			}
   115  			buf = []*logpb.Text_Line{wholeFirstLine}
   116  		}
   117  
   118  		// Process the next lines, which should be all complete with at most one partial at the end.
   119  		wholeLines := txt.Lines[1:]
   120  		var lastPartialLine *logpb.Text_Line
   121  		if lastIdx := len(wholeLines) - 1; lastIdx >= 0 && wholeLines[lastIdx].Delimiter == "" {
   122  			lastPartialLine = wholeLines[lastIdx]
   123  			wholeLines = wholeLines[:lastIdx]
   124  		}
   125  
   126  		for _, line := range wholeLines {
   127  			if line.Delimiter == "" {
   128  				panic(errors.New("partial line not last in LogEntry"))
   129  			}
   130  			buf = append(buf, line)
   131  		}
   132  
   133  		flushBuffer()
   134  
   135  		// If the last line is partial, record it.
   136  		if lastPartialLine != nil {
   137  			buf = []*logpb.Text_Line{lastPartialLine}
   138  		}
   139  	}
   140  }