Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions modules/docker/daemon.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package docker

import (
"os"
"path/filepath"
"strings"

"github.com/docker/docker/pkg/pidfile"
)

// resolvePidFilePath expands "auto" into a guess at the pid file's location, or
// "" when there is no local pid file to read. Set the path explicitly if the
// daemon runs with a --pidfile that isn't next to its socket.
func resolvePidFilePath(configured string, daemonHost string) string {
if configured != "auto" {
return configured
}

socket, ok := strings.CutPrefix(daemonHost, "unix://")
if !ok {
return ""
}

return filepath.Join(filepath.Dir(socket), "docker.pid")
}

// daemonIsRunning reports whether dockerd is up, judged only from its pid file:
// under socket activation, asking the API is what starts it.
func daemonIsRunning(path string) (running bool, known bool) {
if path == "" {
return false, false
}

pid, err := pidfile.Read(path)
if os.IsNotExist(err) {
return false, true
}
if err != nil {
return false, false
}

// pidfile.Read reports 0 for a stale file.
return pid != 0, true
}
62 changes: 62 additions & 0 deletions modules/docker/daemon_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package docker

import (
"os"
"path/filepath"
"strconv"
"testing"

"github.com/stretchr/testify/assert"
)

func Test_resolvePidFilePath(t *testing.T) {
tests := []struct {
name string
configured string
daemonHost string
expected string
}{
{"unset", "", "unix:///var/run/docker.sock", ""},
{"explicit path", "/tmp/docker.pid", "unix:///var/run/docker.sock", "/tmp/docker.pid"},
{"auto, rootful", "auto", "unix:///var/run/docker.sock", "/var/run/docker.pid"},
{"auto, rootless", "auto", "unix:///run/user/1000/docker.sock", "/run/user/1000/docker.pid"},
{"auto, remote", "auto", "tcp://192.168.1.10:2376", ""},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, resolvePidFilePath(tt.configured, tt.daemonHost))
})
}
}

func Test_daemonIsRunning(t *testing.T) {
dir := t.TempDir()

pidFile := func(name, contents string) string {
path := filepath.Join(dir, name)
assert.NoError(t, os.WriteFile(path, []byte(contents), 0o644))
return path
}

tests := []struct {
name string
path string
running bool
known bool
}{
{"no path", "", false, false},
{"absent file", filepath.Join(dir, "absent.pid"), false, true},
{"live pid", pidFile("live.pid", strconv.Itoa(os.Getpid())), true, true},
{"stale pid", pidFile("stale.pid", "0"), false, true},
{"malformed", pidFile("garbage.pid", "nope"), false, true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
running, known := daemonIsRunning(tt.path)
assert.Equal(t, tt.known, known)
assert.Equal(t, tt.running, running)
})
}
}
6 changes: 4 additions & 2 deletions modules/docker/example-conf.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ wtf:
focused: orange
normal: gray
checked: yellow
highlight:
highlight:
fore: black
back: gray
rows:
Expand All @@ -34,4 +34,6 @@ wtf:
width: 3
refreshInterval: 1
labelColor: lightblue

# Read dockerd's pid file instead of the API to check if it's up, so a
# socket-activated daemon isn't started on every refresh. Omit to disable.
pidFilePath: "auto"
7 changes: 6 additions & 1 deletion modules/docker/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,19 @@ const (
type Settings struct {
*cfg.Common

labelColor string
labelColor string
pidFilePath string
}

// NewSettingsFromYAML creates and returns an instance of Settings with configuration options populated
func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {
settings := Settings{
Common: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),
labelColor: ymlConfig.UString("labelColor", "white"),

// Path to dockerd's pid file, read instead of probing the API so a
// socket-activated daemon isn't woken up. "auto" derives it; "" disables.
pidFilePath: ymlConfig.UString("pidFilePath", ""),
}

return &settings
Expand Down
7 changes: 7 additions & 0 deletions modules/docker/widget.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type Widget struct {
view.TextWidget
cli *client.Client
settings *Settings
pidFilePath string
displayBuffer string
}

Expand All @@ -28,6 +29,7 @@ func NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.P
widget.displayBuffer = fmt.Errorf("could not create client: %w", err).Error()
} else {
widget.cli = cli
widget.pidFilePath = resolvePidFilePath(settings.pidFilePath, cli.DaemonHost())
}

widget.refreshDisplayBuffer()
Expand All @@ -53,6 +55,11 @@ func (widget *Widget) refreshDisplayBuffer() {
return
}

if running, known := daemonIsRunning(widget.pidFilePath); known && !running {
widget.displayBuffer = fmt.Sprintf("[%s] docker daemon is not running[white]\n", widget.settings.Colors.Subheading)
return
}

widget.displayBuffer = ""

widget.displayBuffer += fmt.Sprintf("[%s] System[white]\n", widget.settings.Colors.Subheading)
Expand Down