mirror of
https://github.com/qdm12/gluetun.git
synced 2026-05-07 12:30:11 +02:00
feat(vpn): VPN_UP_COMMAND and VPN_DOWN_COMMAND options
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
package command
|
||||
|
||||
type Logger interface {
|
||||
Info(s string)
|
||||
Error(s string)
|
||||
}
|
||||
+11
-11
@@ -9,13 +9,13 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCommandEmpty = errors.New("command is empty")
|
||||
ErrSingleQuoteUnterminated = errors.New("unterminated single-quoted string")
|
||||
ErrDoubleQuoteUnterminated = errors.New("unterminated double-quoted string")
|
||||
ErrEscapeUnterminated = errors.New("unterminated backslash-escape")
|
||||
errCommandEmpty = errors.New("command is empty")
|
||||
errSingleQuoteUnterminated = errors.New("unterminated single-quoted string")
|
||||
errDoubleQuoteUnterminated = errors.New("unterminated double-quoted string")
|
||||
errEscapeUnterminated = errors.New("unterminated backslash-escape")
|
||||
)
|
||||
|
||||
// Split splits a command string into a slice of arguments.
|
||||
// split splits a command string into a slice of arguments.
|
||||
// This is especially important for commands such as:
|
||||
// /bin/sh -c "echo hello"
|
||||
// which should be split into: ["/bin/sh", "-c", "echo hello"]
|
||||
@@ -23,9 +23,9 @@ var (
|
||||
// It does not support:
|
||||
// - the $" quoting style.
|
||||
// - expansion (brace, shell or pathname).
|
||||
func Split(command string) (words []string, err error) {
|
||||
func split(command string) (words []string, err error) {
|
||||
if command == "" {
|
||||
return nil, fmt.Errorf("%w", ErrCommandEmpty)
|
||||
return nil, fmt.Errorf("%w", errCommandEmpty)
|
||||
}
|
||||
|
||||
const bufferSize = 1024
|
||||
@@ -42,7 +42,7 @@ func Split(command string) (words []string, err error) {
|
||||
case character == '\\':
|
||||
// Look ahead to eventually skip an escaped newline
|
||||
if command[startIndex+runeSize:] == "" {
|
||||
return nil, fmt.Errorf("%w: %q", ErrEscapeUnterminated, command)
|
||||
return nil, fmt.Errorf("%w: %q", errEscapeUnterminated, command)
|
||||
}
|
||||
character, runeSize := utf8.DecodeRuneInString(command[startIndex+runeSize:])
|
||||
if character == '\n' {
|
||||
@@ -119,7 +119,7 @@ func handleDoubleQuoted(input string, startIndex int, buffer *bytes.Buffer) (
|
||||
startIndex = cursor
|
||||
}
|
||||
}
|
||||
return "", 0, fmt.Errorf("%w", ErrDoubleQuoteUnterminated)
|
||||
return "", 0, fmt.Errorf("%w", errDoubleQuoteUnterminated)
|
||||
}
|
||||
|
||||
func handleSingleQuoted(input string, startIndex int, buffer *bytes.Buffer) (
|
||||
@@ -127,7 +127,7 @@ func handleSingleQuoted(input string, startIndex int, buffer *bytes.Buffer) (
|
||||
) {
|
||||
closingQuoteIndex := strings.IndexRune(input[startIndex:], '\'')
|
||||
if closingQuoteIndex == -1 {
|
||||
return "", 0, fmt.Errorf("%w", ErrSingleQuoteUnterminated)
|
||||
return "", 0, fmt.Errorf("%w", errSingleQuoteUnterminated)
|
||||
}
|
||||
buffer.WriteString(input[startIndex : startIndex+closingQuoteIndex])
|
||||
const singleQuoteRuneLength = 1
|
||||
@@ -139,7 +139,7 @@ func handleEscaped(input string, startIndex int, buffer *bytes.Buffer) (
|
||||
word string, newStartIndex int, err error,
|
||||
) {
|
||||
if input[startIndex:] == "" {
|
||||
return "", 0, fmt.Errorf("%w", ErrEscapeUnterminated)
|
||||
return "", 0, fmt.Errorf("%w", errEscapeUnterminated)
|
||||
}
|
||||
character, runeLength := utf8.DecodeRuneInString(input[startIndex:])
|
||||
if character != '\n' { // backslash-escaped newline is ignored
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Split(t *testing.T) {
|
||||
func Test_split(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
@@ -17,7 +17,7 @@ func Test_Split(t *testing.T) {
|
||||
}{
|
||||
"empty": {
|
||||
command: "",
|
||||
errWrapped: ErrCommandEmpty,
|
||||
errWrapped: errCommandEmpty,
|
||||
errMessage: "command is empty",
|
||||
},
|
||||
"concrete_sh_command": {
|
||||
@@ -74,22 +74,22 @@ func Test_Split(t *testing.T) {
|
||||
},
|
||||
"unterminated_single_quote": {
|
||||
command: "'abc'\\''def",
|
||||
errWrapped: ErrSingleQuoteUnterminated,
|
||||
errWrapped: errSingleQuoteUnterminated,
|
||||
errMessage: `splitting word in "'abc'\\''def": unterminated single-quoted string`,
|
||||
},
|
||||
"unterminated_double_quote": {
|
||||
command: "\"abc'def",
|
||||
errWrapped: ErrDoubleQuoteUnterminated,
|
||||
errWrapped: errDoubleQuoteUnterminated,
|
||||
errMessage: `splitting word in "\"abc'def": unterminated double-quoted string`,
|
||||
},
|
||||
"unterminated_escape": {
|
||||
command: "abc\\",
|
||||
errWrapped: ErrEscapeUnterminated,
|
||||
errWrapped: errEscapeUnterminated,
|
||||
errMessage: `splitting word in "abc\\": unterminated backslash-escape`,
|
||||
},
|
||||
"unterminated_escape_only": {
|
||||
command: " \\",
|
||||
errWrapped: ErrEscapeUnterminated,
|
||||
errWrapped: errEscapeUnterminated,
|
||||
errMessage: `unterminated backslash-escape: " \\"`,
|
||||
},
|
||||
}
|
||||
@@ -98,7 +98,7 @@ func Test_Split(t *testing.T) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
words, err := Split(testCase.command)
|
||||
words, err := split(testCase.command)
|
||||
|
||||
assert.Equal(t, testCase.words, words)
|
||||
assert.ErrorIs(t, err, testCase.errWrapped)
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func (c *Cmder) RunAndLog(ctx context.Context, command string, logger Logger) (err error) {
|
||||
args, err := split(command)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing command: %w", err)
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, args[0], args[1:]...) // #nosec G204
|
||||
stdout, stderr, waitError, err := c.Start(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
streamCtx, streamCancel := context.WithCancel(context.Background())
|
||||
streamDone := make(chan struct{})
|
||||
go streamLines(streamCtx, streamDone, logger, stdout, stderr)
|
||||
|
||||
err = <-waitError
|
||||
streamCancel()
|
||||
<-streamDone
|
||||
return err
|
||||
}
|
||||
|
||||
func streamLines(ctx context.Context, done chan<- struct{},
|
||||
logger Logger, stdout, stderr <-chan string,
|
||||
) {
|
||||
defer close(done)
|
||||
|
||||
var line string
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case line = <-stdout:
|
||||
logger.Info(line)
|
||||
case line = <-stderr:
|
||||
logger.Error(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user