Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(deps): implement github.com/qdm12/golibs/command locally #2418

Merged
merged 2 commits into from
Aug 21, 2024
Merged
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
13 changes: 10 additions & 3 deletions cmd/gluetun/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"net/http"
"os"
"os/exec"
"os/signal"
"strings"
"syscall"
Expand All @@ -15,6 +16,7 @@ import (
_ "github.com/breml/rootcerts"
"github.com/qdm12/gluetun/internal/alpine"
"github.com/qdm12/gluetun/internal/cli"
"github.com/qdm12/gluetun/internal/command"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/configuration/sources/files"
"github.com/qdm12/gluetun/internal/configuration/sources/secrets"
Expand All @@ -41,7 +43,6 @@ import (
"github.com/qdm12/gluetun/internal/updater/resolver"
"github.com/qdm12/gluetun/internal/updater/unzip"
"github.com/qdm12/gluetun/internal/vpn"
"github.com/qdm12/golibs/command"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/reader/sources/env"
"github.com/qdm12/goshutdown"
Expand Down Expand Up @@ -78,7 +79,7 @@ func main() {
netLinkDebugLogger := logger.New(log.SetComponent("netlink"))
netLinker := netlink.New(netLinkDebugLogger)
cli := cli.New()
cmder := command.NewCmder()
cmder := command.New()

reader := reader.New(reader.Settings{
Sources: []reader.Source{
Expand Down Expand Up @@ -145,7 +146,7 @@ var (
//nolint:gocognit,gocyclo,maintidx
func _main(ctx context.Context, buildInfo models.BuildInformation,
args []string, logger log.LoggerInterface, reader *reader.Reader,
tun Tun, netLinker netLinker, cmder command.RunStarter,
tun Tun, netLinker netLinker, cmder RunStarter,
cli clier) error {
if len(args) > 1 { // cli operation
switch args[1] {
Expand Down Expand Up @@ -591,3 +592,9 @@ type Tun interface {
Check(tunDevice string) error
Create(tunDevice string) error
}

type RunStarter interface {
Run(cmd *exec.Cmd) (output string, err error)
Start(cmd *exec.Cmd) (stdoutLines, stderrLines <-chan string,
waitError <-chan error, err error)
}
8 changes: 8 additions & 0 deletions internal/command/cmder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package command

// Cmder handles running subprograms synchronously and asynchronously.
type Cmder struct{}

func New() *Cmder {
return &Cmder{}
}
11 changes: 11 additions & 0 deletions internal/command/interfaces_local.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package command

import "io"

type execCmd interface {
CombinedOutput() ([]byte, error)
StdoutPipe() (io.ReadCloser, error)
StderrPipe() (io.ReadCloser, error)
Start() error
Wait() error
}
3 changes: 3 additions & 0 deletions internal/command/mocks_generate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package command

//go:generate mockgen -destination=mocks_local_test.go -package=$GOPACKAGE -source=interfaces_local.go
108 changes: 108 additions & 0 deletions internal/command/mocks_local_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions internal/command/run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package command

import (
"os/exec"
"strings"
)

// Run runs a command in a blocking manner, returning its output and
// an error if it failed.
func (c *Cmder) Run(cmd *exec.Cmd) (output string, err error) {
return run(cmd)
}

func run(cmd execCmd) (output string, err error) {
stdout, err := cmd.CombinedOutput()
output = string(stdout)
output = strings.TrimSuffix(output, "\n")
lines := stringToLines(output)
for i := range lines {
lines[i] = strings.TrimPrefix(lines[i], "'")
lines[i] = strings.TrimSuffix(lines[i], "'")
}
output = strings.Join(lines, "\n")
return output, err
}

func stringToLines(s string) (lines []string) {
s = strings.TrimSuffix(s, "\n")
return strings.Split(s, "\n")
}
55 changes: 55 additions & 0 deletions internal/command/run_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package command

import (
"errors"
"testing"

gomock "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func Test_run(t *testing.T) {
t.Parallel()

errDummy := errors.New("dummy")

testCases := map[string]struct {
stdout []byte
cmdErr error
output string
err error
}{
"no output": {},
"cmd error": {
stdout: []byte("'hello \nworld'\n"),
cmdErr: errDummy,
output: "hello \nworld",
err: errDummy,
},
}

for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()

ctrl := gomock.NewController(t)

mockCmd := NewMockexecCmd(ctrl)

mockCmd.EXPECT().CombinedOutput().Return(testCase.stdout, testCase.cmdErr)

output, err := run(mockCmd)

if testCase.err != nil {
require.Error(t, err)
assert.Equal(t, testCase.err.Error(), err.Error())
} else {
assert.NoError(t, err)
}

assert.Equal(t, testCase.output, output)
})
}
}
97 changes: 97 additions & 0 deletions internal/command/start.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package command

import (
"bufio"
"errors"
"io"
"os"
"os/exec"
)

// Start launches a command and streams stdout and stderr to channels.
// All the channels returned are ready only and won't be closed
// if the command fails later.
func (c *Cmder) Start(cmd *exec.Cmd) (
stdoutLines, stderrLines <-chan string,
waitError <-chan error, startErr error) {
return start(cmd)
}

func start(cmd execCmd) (stdoutLines, stderrLines <-chan string,
waitError <-chan error, startErr error) {
stop := make(chan struct{})
stdoutReady := make(chan struct{})
stdoutLinesCh := make(chan string)
stdoutDone := make(chan struct{})
stderrReady := make(chan struct{})
stderrLinesCh := make(chan string)
stderrDone := make(chan struct{})

stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, nil, nil, err
}
go streamToChannel(stdoutReady, stop, stdoutDone, stdout, stdoutLinesCh)

stderr, err := cmd.StderrPipe()
if err != nil {
_ = stdout.Close()
close(stop)
<-stdoutDone
return nil, nil, nil, err
}
go streamToChannel(stderrReady, stop, stderrDone, stderr, stderrLinesCh)

err = cmd.Start()
if err != nil {
_ = stdout.Close()
_ = stderr.Close()
close(stop)
<-stdoutDone
<-stderrDone
return nil, nil, nil, err
}

waitErrorCh := make(chan error)
go func() {
err := cmd.Wait()
_ = stdout.Close()
_ = stderr.Close()
close(stop)
<-stdoutDone
<-stderrDone
waitErrorCh <- err
}()

return stdoutLinesCh, stderrLinesCh, waitErrorCh, nil
}

func streamToChannel(ready chan<- struct{},
stop <-chan struct{}, done chan<- struct{},
stream io.Reader, lines chan<- string) {
defer close(done)
close(ready)
scanner := bufio.NewScanner(stream)
lineBuffer := make([]byte, bufio.MaxScanTokenSize) // 64KB
const maxCapacity = 20 * 1024 * 1024 // 20MB
scanner.Buffer(lineBuffer, maxCapacity)

for scanner.Scan() {
// scanner is closed if the context is canceled
// or if the command failed starting because the
// stream is closed (io.EOF error).
lines <- scanner.Text()
}
err := scanner.Err()
if err == nil || errors.Is(err, os.ErrClosed) {
return
}

// ignore the error if it is stopped.
select {
case <-stop:
return
default:
lines <- "stream error: " + err.Error()
}
}
Loading
Loading