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 }