-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Expand file tree
/
Copy pathcontainer_logs.go
More file actions
233 lines (199 loc) · 6.09 KB
/
Copy pathcontainer_logs.go
File metadata and controls
233 lines (199 loc) · 6.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
package gui
import (
"context"
"fmt"
"io"
"os"
"os/signal"
"strconv"
"strings"
"time"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/stdcopy"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazydocker/pkg/commands"
"github.com/jesseduffield/lazydocker/pkg/tasks"
"github.com/jesseduffield/lazydocker/pkg/utils"
)
// containerLogsTailPresets are the values we cycle through when the user asks to
// change how many lines of logs are displayed. An empty string means 'show all logs'.
var containerLogsTailPresets = []string{"", "50", "100", "200", "500"}
func containerLogsTailLabel(tail string) string {
if tail == "" {
return "all"
}
return tail
}
// cycleContainerLogsTail moves to the next tail preset (wrapping back to the start)
// and forces the logs tab to re-render with the new value.
func (gui *Gui) cycleContainerLogsTail(g *gocui.Gui, v *gocui.View) error {
currentIdx := 0
for i, preset := range containerLogsTailPresets {
if preset == gui.State.ContainerLogsTail {
currentIdx = i
break
}
}
gui.State.ContainerLogsTail = containerLogsTailPresets[(currentIdx+1)%len(containerLogsTailPresets)]
if v != nil && v.Name() == "services" {
return gui.Panels.Services.HandleSelect()
}
return gui.Panels.Containers.HandleSelect()
}
// tailLimitingWriter wraps a writer and re-renders it on every line so that it only
// ever displays the last maxLines lines, keeping a true sliding window rather than
// just applying `--tail` to the initial snapshot and then growing forever as new
// lines are followed in.
type tailLimitingWriter struct {
view *gocui.View
maxLines int
lines []string
partial string
}
func newTailLimitingWriter(view *gocui.View, maxLines int) *tailLimitingWriter {
return &tailLimitingWriter{view: view, maxLines: maxLines}
}
func (w *tailLimitingWriter) Write(p []byte) (int, error) {
w.partial += string(p)
split := strings.Split(w.partial, "\n")
w.partial = split[len(split)-1]
newLines := split[:len(split)-1]
if len(newLines) == 0 {
return len(p), nil
}
w.lines = append(w.lines, newLines...)
if excess := len(w.lines) - w.maxLines; excess > 0 {
w.lines = w.lines[excess:]
}
// SetContent clears and rewrites atomically, so the view never renders empty
// in between
w.view.SetContent(strings.Join(w.lines, "\n") + "\n")
return len(p), nil
}
func (gui *Gui) renderContainerLogsToMain(container *commands.Container) tasks.TaskFunc {
return gui.NewTickerTask(TickerTaskOpts{
Func: func(ctx context.Context, notifyStopped chan struct{}) {
gui.renderContainerLogsToMainAux(container, ctx, notifyStopped)
},
Duration: time.Millisecond * 200,
// TODO: see why this isn't working (when switching from Top tab to Logs tab in the services panel, the tops tab's content isn't removed)
Before: func(ctx context.Context) { gui.clearMainView() },
Wrap: gui.Config.UserConfig.Gui.WrapMainPanel,
Autoscroll: true,
})
}
func (gui *Gui) renderContainerLogsToMainAux(container *commands.Container, ctx context.Context, notifyStopped chan struct{}) {
gui.clearMainView()
defer func() {
notifyStopped <- struct{}{}
}()
mainView := gui.Views.Main
tail := gui.State.ContainerLogsTail
mainView.Subtitle = fmt.Sprintf("tail: %s (%s to cycle)", containerLogsTailLabel(tail), "t")
var writer io.Writer = mainView
if maxLines, err := strconv.Atoi(tail); err == nil && maxLines > 0 {
// keep re-truncating to the chosen number of lines as new ones stream in,
// instead of just limiting the initial snapshot and growing unbounded after that
writer = newTailLimitingWriter(mainView, maxLines)
}
if err := gui.writeContainerLogs(container, ctx, writer); err != nil {
gui.Log.Error(err)
}
// if we are here because the task has been stopped, we should return
// if we are here then the container must have exited, meaning we should wait until it's back again before
ticker := time.NewTicker(time.Millisecond * 100)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
result, err := container.Inspect()
if err != nil {
// if we get an error, then the container has probably been removed so we'll get out of here
gui.Log.Error(err)
return
}
if result.State.Running {
return
}
}
}
}
func (gui *Gui) renderLogsToStdout(container *commands.Container) {
stop := make(chan os.Signal, 1)
defer signal.Stop(stop)
ctx, cancel := context.WithCancel(context.Background())
go func() {
signal.Notify(stop, os.Interrupt)
<-stop
cancel()
}()
if err := gui.g.Suspend(); err != nil {
gui.Log.Error(err)
return
}
defer func() {
if err := gui.g.Resume(); err != nil {
gui.Log.Error(err)
}
}()
if err := gui.writeContainerLogs(container, ctx, os.Stdout); err != nil {
gui.Log.Error(err)
return
}
gui.promptToReturn()
}
func (gui *Gui) promptToReturn() {
if !gui.Config.UserConfig.Gui.ReturnImmediately {
fmt.Fprintf(os.Stdout, "\n\n%s", utils.ColoredString(gui.Tr.PressEnterToReturn, color.FgGreen))
// wait for enter press
if _, err := fmt.Scanln(); err != nil {
gui.Log.Error(err)
}
}
}
func (gui *Gui) writeContainerLogs(ctr *commands.Container, ctx context.Context, writer io.Writer) error {
readCloser, err := gui.DockerCommand.Client.ContainerLogs(ctx, ctr.ID, container.LogsOptions{
ShowStdout: true,
ShowStderr: true,
Timestamps: gui.Config.UserConfig.Logs.Timestamps,
Since: gui.Config.UserConfig.Logs.Since,
Tail: gui.State.ContainerLogsTail,
Follow: true,
})
if err != nil {
gui.Log.Error(err)
return err
}
defer readCloser.Close()
if !ctr.DetailsLoaded() {
// loop until the details load or context is cancelled, using timer
ticker := time.NewTicker(time.Millisecond * 100)
defer ticker.Stop()
outer:
for {
select {
case <-ctx.Done():
return nil
case <-ticker.C:
if ctr.DetailsLoaded() {
break outer
}
}
}
}
if ctr.Details.Config.Tty {
_, err = io.Copy(writer, readCloser)
if err != nil {
return err
}
} else {
_, err = stdcopy.StdCopy(writer, writer, readCloser)
if err != nil {
return err
}
}
return nil
}