mirror of
https://git.vectorsigma.ru/public/AdGuardHome.git
synced 2026-08-03 19:29:41 +00:00
all: imp code
This commit is contained in:
@@ -3,6 +3,12 @@ package agh
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/osutil"
|
||||||
|
"github.com/AdguardTeam/golibs/osutil/executil"
|
||||||
|
"github.com/AdguardTeam/golibs/testutil/fakeos/fakeexec"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConfigModifier defines an interface for updating the global configuration.
|
// ConfigModifier defines an interface for updating the global configuration.
|
||||||
@@ -20,3 +26,124 @@ var _ ConfigModifier = EmptyConfigModifier{}
|
|||||||
|
|
||||||
// Apply implements the [ConfigModifier] for EmptyConfigModifier.
|
// Apply implements the [ConfigModifier] for EmptyConfigModifier.
|
||||||
func (em EmptyConfigModifier) Apply(ctx context.Context) {}
|
func (em EmptyConfigModifier) Apply(ctx context.Context) {}
|
||||||
|
|
||||||
|
// TODO(s.chzhen): !! Is there another way?
|
||||||
|
//
|
||||||
|
// TODO(s.chzhen): !! Docs, naming.
|
||||||
|
//
|
||||||
|
// TODO(s.chzhen): Move to aghtest once the import cycle is resolved.
|
||||||
|
type exitErr struct {
|
||||||
|
code osutil.ExitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ executil.ExitCodeError = exitErr{}
|
||||||
|
|
||||||
|
func (e exitErr) Error() (s string) {
|
||||||
|
return fmt.Sprintf("exit code %d", e.code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e exitErr) ExitCode() (code osutil.ExitCode) {
|
||||||
|
return e.code
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExternalCommand struct {
|
||||||
|
Err error
|
||||||
|
Cmd string
|
||||||
|
Out string
|
||||||
|
Code int
|
||||||
|
}
|
||||||
|
|
||||||
|
func keyCommand(path string, args []string) (k string) {
|
||||||
|
return path + " " + strings.Join(args, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCommand(s string) (path string, args []string) {
|
||||||
|
f := strings.Fields(s)
|
||||||
|
if len(f) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return f[0], f[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMultipleCommandConstructor is a helper function that returns a mock
|
||||||
|
// [executil.CommandConstructor] for tests.
|
||||||
|
func NewMultipleCommandConstructor(cmds ...ExternalCommand) (cs executil.CommandConstructor) {
|
||||||
|
table := make(map[string]ExternalCommand, len(cmds))
|
||||||
|
for _, ec := range cmds {
|
||||||
|
p, a := parseCommand(ec.Cmd)
|
||||||
|
table[keyCommand(p, a)] = ec
|
||||||
|
}
|
||||||
|
|
||||||
|
return &fakeexec.CommandConstructor{
|
||||||
|
OnNew: func(
|
||||||
|
_ context.Context,
|
||||||
|
conf *executil.CommandConfig,
|
||||||
|
) (c executil.Command, err error) {
|
||||||
|
ec := table[keyCommand(conf.Path, conf.Args)]
|
||||||
|
|
||||||
|
cmd := fakeexec.NewCommand()
|
||||||
|
cmd.OnStart = func(_ context.Context) (err error) {
|
||||||
|
if ec.Out != "" {
|
||||||
|
_, _ = conf.Stdout.Write([]byte(ec.Out))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.OnWait = func(_ context.Context) (err error) {
|
||||||
|
if ec.Err != nil {
|
||||||
|
return ec.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ec.Code != 0 {
|
||||||
|
return exitErr{code: ec.Code}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCommandConstructor is a helper function that returns a mock
|
||||||
|
// [executil.CommandConstructor] for tests.
|
||||||
|
func NewCommandConstructor(
|
||||||
|
_ string,
|
||||||
|
code int,
|
||||||
|
stdout string,
|
||||||
|
cmdErr error,
|
||||||
|
) (cs executil.CommandConstructor) {
|
||||||
|
return &fakeexec.CommandConstructor{
|
||||||
|
OnNew: func(
|
||||||
|
_ context.Context,
|
||||||
|
conf *executil.CommandConfig,
|
||||||
|
) (c executil.Command, err error) {
|
||||||
|
cmd := fakeexec.NewCommand()
|
||||||
|
cmd.OnStart = func(_ context.Context) (err error) {
|
||||||
|
if conf.Stdout != nil {
|
||||||
|
_, _ = conf.Stdout.Write([]byte(stdout))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.OnWait = func(_ context.Context) (err error) {
|
||||||
|
if cmdErr != nil {
|
||||||
|
return cmdErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if code != 0 {
|
||||||
|
return exitErr{code: code}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
|
||||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
"github.com/AdguardTeam/golibs/osutil"
|
"github.com/AdguardTeam/golibs/osutil"
|
||||||
|
"github.com/AdguardTeam/golibs/osutil/executil"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DialContextFunc is the semantic alias for dialing functions, such as
|
// DialContextFunc is the semantic alias for dialing functions, such as
|
||||||
@@ -26,9 +26,6 @@ type DialContextFunc = func(ctx context.Context, network, addr string) (conn net
|
|||||||
|
|
||||||
// Variables and functions to substitute in tests.
|
// Variables and functions to substitute in tests.
|
||||||
var (
|
var (
|
||||||
// aghosRunCommand is the function to run shell commands.
|
|
||||||
aghosRunCommand = aghos.RunCommand
|
|
||||||
|
|
||||||
// netInterfaces is the function to get the available network interfaces.
|
// netInterfaces is the function to get the available network interfaces.
|
||||||
netInterfaceAddrs = net.InterfaceAddrs
|
netInterfaceAddrs = net.InterfaceAddrs
|
||||||
|
|
||||||
@@ -43,32 +40,53 @@ const ErrNoStaticIPInfo errors.Error = "no information about static ip"
|
|||||||
// IfaceHasStaticIP checks if interface is configured to have static IP address.
|
// IfaceHasStaticIP checks if interface is configured to have static IP address.
|
||||||
// If it can't give a definitive answer, it returns false and an error for which
|
// If it can't give a definitive answer, it returns false and an error for which
|
||||||
// errors.Is(err, ErrNoStaticIPInfo) is true.
|
// errors.Is(err, ErrNoStaticIPInfo) is true.
|
||||||
func IfaceHasStaticIP(ifaceName string) (has bool, err error) {
|
func IfaceHasStaticIP(
|
||||||
return ifaceHasStaticIP(ifaceName)
|
ctx context.Context,
|
||||||
|
cmdCons executil.CommandConstructor,
|
||||||
|
ifaceName string,
|
||||||
|
) (has bool, err error) {
|
||||||
|
return ifaceHasStaticIP(ctx, cmdCons, ifaceName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IfaceSetStaticIP sets static IP address for network interface.
|
// IfaceSetStaticIP sets static IP address for network interface.
|
||||||
func IfaceSetStaticIP(ifaceName string) (err error) {
|
func IfaceSetStaticIP(
|
||||||
return ifaceSetStaticIP(ifaceName)
|
ctx context.Context,
|
||||||
|
cmdCons executil.CommandConstructor,
|
||||||
|
ifaceName string,
|
||||||
|
) (err error) {
|
||||||
|
return ifaceSetStaticIP(ctx, cmdCons, ifaceName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GatewayIP returns IP address of interface's gateway.
|
// GatewayIP returns IP address of interface's gateway.
|
||||||
//
|
//
|
||||||
// TODO(e.burkov): Investigate if the gateway address may be fetched in another
|
// TODO(e.burkov): Investigate if the gateway address may be fetched in another
|
||||||
// way since not every machine has the software installed.
|
// way since not every machine has the software installed.
|
||||||
func GatewayIP(ifaceName string) (ip netip.Addr) {
|
func GatewayIP(
|
||||||
code, out, err := aghosRunCommand("ip", "route", "show", "dev", ifaceName)
|
ctx context.Context,
|
||||||
|
cmdCons executil.CommandConstructor,
|
||||||
|
ifaceName string,
|
||||||
|
) (ip netip.Addr) {
|
||||||
|
stdout := bytes.Buffer{}
|
||||||
|
err := executil.Run(
|
||||||
|
ctx,
|
||||||
|
cmdCons,
|
||||||
|
&executil.CommandConfig{
|
||||||
|
Path: "ip",
|
||||||
|
Args: []string{"route", "show", "dev", ifaceName},
|
||||||
|
Stdout: &stdout,
|
||||||
|
},
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug("%s", err)
|
if code, ok := executil.ExitCodeFromError(err); ok {
|
||||||
|
|
||||||
return netip.Addr{}
|
|
||||||
} else if code != 0 {
|
|
||||||
log.Debug("fetching gateway ip: unexpected exit code: %d", code)
|
log.Debug("fetching gateway ip: unexpected exit code: %d", code)
|
||||||
|
} else {
|
||||||
|
log.Debug("%s", err)
|
||||||
|
}
|
||||||
|
|
||||||
return netip.Addr{}
|
return netip.Addr{}
|
||||||
}
|
}
|
||||||
|
|
||||||
fields := bytes.Fields(out)
|
fields := bytes.Fields(stdout.Bytes())
|
||||||
// The meaningful "ip route" command output should contain the word
|
// The meaningful "ip route" command output should contain the word
|
||||||
// "default" at first field and default gateway IP address at third field.
|
// "default" at first field and default gateway IP address at third field.
|
||||||
if len(fields) < 3 || string(fields[0]) != "default" {
|
if len(fields) < 3 || string(fields[0]) != "default" {
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ package aghnet
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/AdguardTeam/golibs/osutil/executil"
|
||||||
)
|
)
|
||||||
|
|
||||||
// hardwarePortInfo contains information about the current state of the internet
|
// hardwarePortInfo contains information about the current state of the internet
|
||||||
@@ -23,8 +25,12 @@ type hardwarePortInfo struct {
|
|||||||
static bool
|
static bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func ifaceHasStaticIP(ifaceName string) (ok bool, err error) {
|
func ifaceHasStaticIP(
|
||||||
portInfo, err := getCurrentHardwarePortInfo(ifaceName)
|
ctx context.Context,
|
||||||
|
cmdCons executil.CommandConstructor,
|
||||||
|
ifaceName string,
|
||||||
|
) (ok bool, err error) {
|
||||||
|
portInfo, err := getCurrentHardwarePortInfo(ctx, cmdCons, ifaceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -34,15 +40,19 @@ func ifaceHasStaticIP(ifaceName string) (ok bool, err error) {
|
|||||||
|
|
||||||
// getCurrentHardwarePortInfo gets information for the specified network
|
// getCurrentHardwarePortInfo gets information for the specified network
|
||||||
// interface.
|
// interface.
|
||||||
func getCurrentHardwarePortInfo(ifaceName string) (hardwarePortInfo, error) {
|
func getCurrentHardwarePortInfo(
|
||||||
|
ctx context.Context,
|
||||||
|
cmdCons executil.CommandConstructor,
|
||||||
|
ifaceName string,
|
||||||
|
) (hardwarePortInfo, error) {
|
||||||
// First of all we should find hardware port name.
|
// First of all we should find hardware port name.
|
||||||
m := getNetworkSetupHardwareReports()
|
m := getNetworkSetupHardwareReports(ctx, cmdCons)
|
||||||
hardwarePort, ok := m[ifaceName]
|
hardwarePort, ok := m[ifaceName]
|
||||||
if !ok {
|
if !ok {
|
||||||
return hardwarePortInfo{}, fmt.Errorf("could not find hardware port for %s", ifaceName)
|
return hardwarePortInfo{}, fmt.Errorf("could not find hardware port for %s", ifaceName)
|
||||||
}
|
}
|
||||||
|
|
||||||
return getHardwarePortInfo(hardwarePort)
|
return getHardwarePortInfo(ctx, cmdCons, hardwarePort)
|
||||||
}
|
}
|
||||||
|
|
||||||
// hardwareReportsReg is the regular expression matching the lines of
|
// hardwareReportsReg is the regular expression matching the lines of
|
||||||
@@ -57,8 +67,12 @@ var hardwareReportsReg = regexp.MustCompile("Hardware Port: (.*?)\nDevice: (.*?)
|
|||||||
// TODO(e.burkov): There should be more proper approach than parsing the
|
// TODO(e.burkov): There should be more proper approach than parsing the
|
||||||
// command output. For example, see
|
// command output. For example, see
|
||||||
// https://developer.apple.com/documentation/systemconfiguration.
|
// https://developer.apple.com/documentation/systemconfiguration.
|
||||||
func getNetworkSetupHardwareReports() (reports map[string]string) {
|
func getNetworkSetupHardwareReports(
|
||||||
_, out, err := aghosRunCommand("networksetup", "-listallhardwareports")
|
ctx context.Context,
|
||||||
|
cmdCons executil.CommandConstructor,
|
||||||
|
) (reports map[string]string) {
|
||||||
|
// TODO(s.chzhen): Pass context.
|
||||||
|
_, out, err := aghos.RunCommand(ctx, cmdCons, "networksetup", "-listallhardwareports")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -77,8 +91,12 @@ func getNetworkSetupHardwareReports() (reports map[string]string) {
|
|||||||
// command output lines containing the port information.
|
// command output lines containing the port information.
|
||||||
var hardwarePortReg = regexp.MustCompile("IP address: (.*?)\nSubnet mask: (.*?)\nRouter: (.*?)\n")
|
var hardwarePortReg = regexp.MustCompile("IP address: (.*?)\nSubnet mask: (.*?)\nRouter: (.*?)\n")
|
||||||
|
|
||||||
func getHardwarePortInfo(hardwarePort string) (h hardwarePortInfo, err error) {
|
func getHardwarePortInfo(
|
||||||
_, out, err := aghosRunCommand("networksetup", "-getinfo", hardwarePort)
|
ctx context.Context,
|
||||||
|
cmdCons executil.CommandConstructor,
|
||||||
|
hardwarePort string,
|
||||||
|
) (h hardwarePortInfo, err error) {
|
||||||
|
_, out, err := aghos.RunCommand(ctx, cmdCons, "networksetup", "-getinfo", hardwarePort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return h, err
|
return h, err
|
||||||
}
|
}
|
||||||
@@ -97,8 +115,12 @@ func getHardwarePortInfo(hardwarePort string) (h hardwarePortInfo, err error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ifaceSetStaticIP(ifaceName string) (err error) {
|
func ifaceSetStaticIP(
|
||||||
portInfo, err := getCurrentHardwarePortInfo(ifaceName)
|
ctx context.Context,
|
||||||
|
cmdCons executil.CommandConstructor,
|
||||||
|
ifaceName string,
|
||||||
|
) (err error) {
|
||||||
|
portInfo, err := getCurrentHardwarePortInfo(ctx, cmdCons, ifaceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -115,7 +137,7 @@ func ifaceSetStaticIP(ifaceName string) (err error) {
|
|||||||
args := append([]string{"-setdnsservers", portInfo.name}, dnsAddrs...)
|
args := append([]string{"-setdnsservers", portInfo.name}, dnsAddrs...)
|
||||||
|
|
||||||
// Setting DNS servers is necessary when configuring a static IP
|
// Setting DNS servers is necessary when configuring a static IP
|
||||||
code, _, err := aghosRunCommand("networksetup", args...)
|
code, _, err := aghos.RunCommand(ctx, cmdCons, "networksetup", args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if code != 0 {
|
} else if code != 0 {
|
||||||
@@ -123,7 +145,9 @@ func ifaceSetStaticIP(ifaceName string) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Actually configures hardware port to have static IP
|
// Actually configures hardware port to have static IP
|
||||||
code, _, err = aghosRunCommand(
|
code, _, err = aghos.RunCommand(
|
||||||
|
ctx,
|
||||||
|
cmdCons,
|
||||||
"networksetup",
|
"networksetup",
|
||||||
"-setmanual",
|
"-setmanual",
|
||||||
portInfo.name,
|
portInfo.name,
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"testing/fstest"
|
"testing/fstest"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/agh"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/AdguardTeam/golibs/osutil/executil"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/AdguardTeam/golibs/testutil/fakefs"
|
"github.com/AdguardTeam/golibs/testutil/fakefs"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -16,48 +18,46 @@ import (
|
|||||||
func TestIfaceHasStaticIP(t *testing.T) {
|
func TestIfaceHasStaticIP(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
shell mapShell
|
cmdCons executil.CommandConstructor
|
||||||
ifaceName string
|
ifaceName string
|
||||||
wantHas assert.BoolAssertionFunc
|
wantHas assert.BoolAssertionFunc
|
||||||
wantErrMsg string
|
wantErrMsg string
|
||||||
}{{
|
}{{
|
||||||
name: "success",
|
name: "success",
|
||||||
shell: mapShell{
|
cmdCons: agh.NewMultipleCommandConstructor(agh.ExternalCommand{
|
||||||
"networksetup -listallhardwareports": {
|
Cmd: "networksetup -listallhardwareports",
|
||||||
err: nil,
|
Err: nil,
|
||||||
out: "Hardware Port: hwport\nDevice: en0\n",
|
Out: "Hardware Port: hwport\nDevice: en0\n",
|
||||||
code: 0,
|
Code: 0,
|
||||||
},
|
}, agh.ExternalCommand{
|
||||||
"networksetup -getinfo hwport": {
|
Cmd: "networksetup -getinfo hwport",
|
||||||
err: nil,
|
Err: nil,
|
||||||
out: "IP address: 1.2.3.4\nSubnet mask: 255.255.255.0\nRouter: 1.2.3.1\n",
|
Out: "IP address: 1.2.3.4\nSubnet mask: 255.255.255.0\nRouter: 1.2.3.1\n",
|
||||||
code: 0,
|
Code: 0,
|
||||||
},
|
}),
|
||||||
},
|
|
||||||
ifaceName: "en0",
|
ifaceName: "en0",
|
||||||
wantHas: assert.False,
|
wantHas: assert.False,
|
||||||
wantErrMsg: ``,
|
wantErrMsg: ``,
|
||||||
}, {
|
}, {
|
||||||
name: "success_static",
|
name: "success_static",
|
||||||
shell: mapShell{
|
cmdCons: agh.NewMultipleCommandConstructor(agh.ExternalCommand{
|
||||||
"networksetup -listallhardwareports": {
|
Cmd: "networksetup -listallhardwareports",
|
||||||
err: nil,
|
Err: nil,
|
||||||
out: "Hardware Port: hwport\nDevice: en0\n",
|
Out: "Hardware Port: hwport\nDevice: en0\n",
|
||||||
code: 0,
|
Code: 0,
|
||||||
},
|
}, agh.ExternalCommand{
|
||||||
"networksetup -getinfo hwport": {
|
Cmd: "networksetup -getinfo hwport",
|
||||||
err: nil,
|
Err: nil,
|
||||||
out: "Manual Configuration\nIP address: 1.2.3.4\n" +
|
Out: "Manual Configuration\nIP address: 1.2.3.4\n" +
|
||||||
"Subnet mask: 255.255.255.0\nRouter: 1.2.3.1\n",
|
"Subnet mask: 255.255.255.0\nRouter: 1.2.3.1\n",
|
||||||
code: 0,
|
Code: 0,
|
||||||
},
|
}),
|
||||||
},
|
|
||||||
ifaceName: "en0",
|
ifaceName: "en0",
|
||||||
wantHas: assert.True,
|
wantHas: assert.True,
|
||||||
wantErrMsg: ``,
|
wantErrMsg: ``,
|
||||||
}, {
|
}, {
|
||||||
name: "reports_error",
|
name: "reports_error",
|
||||||
shell: theOnlyCmd(
|
cmdCons: agh.NewCommandConstructor(
|
||||||
"networksetup -listallhardwareports",
|
"networksetup -listallhardwareports",
|
||||||
0,
|
0,
|
||||||
"",
|
"",
|
||||||
@@ -68,35 +68,33 @@ func TestIfaceHasStaticIP(t *testing.T) {
|
|||||||
wantErrMsg: `could not find hardware port for en0`,
|
wantErrMsg: `could not find hardware port for en0`,
|
||||||
}, {
|
}, {
|
||||||
name: "port_error",
|
name: "port_error",
|
||||||
shell: mapShell{
|
cmdCons: agh.NewMultipleCommandConstructor(agh.ExternalCommand{
|
||||||
"networksetup -listallhardwareports": {
|
Cmd: "networksetup -listallhardwareports",
|
||||||
err: nil,
|
Err: nil,
|
||||||
out: "Hardware Port: hwport\nDevice: en0\n",
|
Out: "Hardware Port: hwport\nDevice: en0\n",
|
||||||
code: 0,
|
Code: 0,
|
||||||
},
|
}, agh.ExternalCommand{
|
||||||
"networksetup -getinfo hwport": {
|
Cmd: "networksetup -getinfo hwport",
|
||||||
err: errors.Error("can't get"),
|
Err: errors.Error("can't get"),
|
||||||
out: ``,
|
Out: ``,
|
||||||
code: 0,
|
Code: 0,
|
||||||
},
|
}),
|
||||||
},
|
|
||||||
ifaceName: "en0",
|
ifaceName: "en0",
|
||||||
wantHas: assert.False,
|
wantHas: assert.False,
|
||||||
wantErrMsg: `can't get`,
|
wantErrMsg: `can't get`,
|
||||||
}, {
|
}, {
|
||||||
name: "port_bad_output",
|
name: "port_bad_output",
|
||||||
shell: mapShell{
|
cmdCons: agh.NewMultipleCommandConstructor(agh.ExternalCommand{
|
||||||
"networksetup -listallhardwareports": {
|
Cmd: "networksetup -listallhardwareports",
|
||||||
err: nil,
|
Err: nil,
|
||||||
out: "Hardware Port: hwport\nDevice: en0\n",
|
Out: "Hardware Port: hwport\nDevice: en0\n",
|
||||||
code: 0,
|
Code: 0,
|
||||||
},
|
}, agh.ExternalCommand{
|
||||||
"networksetup -getinfo hwport": {
|
Cmd: "networksetup -getinfo hwport",
|
||||||
err: nil,
|
Err: nil,
|
||||||
out: "nothing meaningful",
|
Out: "nothing meaningful",
|
||||||
code: 0,
|
Code: 0,
|
||||||
},
|
}),
|
||||||
},
|
|
||||||
ifaceName: "en0",
|
ifaceName: "en0",
|
||||||
wantHas: assert.False,
|
wantHas: assert.False,
|
||||||
wantErrMsg: `could not find hardware port info`,
|
wantErrMsg: `could not find hardware port info`,
|
||||||
@@ -104,9 +102,8 @@ func TestIfaceHasStaticIP(t *testing.T) {
|
|||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
substShell(t, tc.shell.RunCmd)
|
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||||
|
has, err := IfaceHasStaticIP(ctx, tc.cmdCons, tc.ifaceName)
|
||||||
has, err := IfaceHasStaticIP(tc.ifaceName)
|
|
||||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||||
|
|
||||||
tc.wantHas(t, has)
|
tc.wantHas(t, has)
|
||||||
@@ -126,55 +123,53 @@ func TestIfaceSetStaticIP(t *testing.T) {
|
|||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
shell mapShell
|
cmdCons executil.CommandConstructor
|
||||||
fsys fs.FS
|
fsys fs.FS
|
||||||
wantErrMsg string
|
wantErrMsg string
|
||||||
}{{
|
}{{
|
||||||
name: "success",
|
name: "success",
|
||||||
shell: mapShell{
|
cmdCons: agh.NewMultipleCommandConstructor(agh.ExternalCommand{
|
||||||
"networksetup -listallhardwareports": {
|
Cmd: "networksetup -listallhardwareports",
|
||||||
err: nil,
|
Err: nil,
|
||||||
out: "Hardware Port: hwport\nDevice: en0\n",
|
Out: "Hardware Port: hwport\nDevice: en0\n",
|
||||||
code: 0,
|
Code: 0,
|
||||||
},
|
}, agh.ExternalCommand{
|
||||||
"networksetup -getinfo hwport": {
|
Cmd: "networksetup -getinfo hwport",
|
||||||
err: nil,
|
Err: nil,
|
||||||
out: "IP address: 1.2.3.4\nSubnet mask: 255.255.255.0\nRouter: 1.2.3.1\n",
|
Out: "IP address: 1.2.3.4\nSubnet mask: 255.255.255.0\nRouter: 1.2.3.1\n",
|
||||||
code: 0,
|
Code: 0,
|
||||||
},
|
}, agh.ExternalCommand{
|
||||||
"networksetup -setdnsservers hwport 1.1.1.1": {
|
Cmd: "networksetup -setdnsservers hwport 1.1.1.1",
|
||||||
err: nil,
|
Err: nil,
|
||||||
out: "",
|
Out: "",
|
||||||
code: 0,
|
Code: 0,
|
||||||
},
|
}, agh.ExternalCommand{
|
||||||
"networksetup -setmanual hwport 1.2.3.4 255.255.255.0 1.2.3.1": {
|
Cmd: "networksetup -setmanual hwport 1.2.3.4 255.255.255.0 1.2.3.1",
|
||||||
err: nil,
|
Err: nil,
|
||||||
out: "",
|
Out: "",
|
||||||
code: 0,
|
Code: 0,
|
||||||
},
|
}),
|
||||||
},
|
|
||||||
fsys: succFsys,
|
fsys: succFsys,
|
||||||
wantErrMsg: ``,
|
wantErrMsg: ``,
|
||||||
}, {
|
}, {
|
||||||
name: "static_already",
|
name: "static_already",
|
||||||
shell: mapShell{
|
cmdCons: agh.NewMultipleCommandConstructor(agh.ExternalCommand{
|
||||||
"networksetup -listallhardwareports": {
|
Cmd: "networksetup -listallhardwareports",
|
||||||
err: nil,
|
Err: nil,
|
||||||
out: "Hardware Port: hwport\nDevice: en0\n",
|
Out: "Hardware Port: hwport\nDevice: en0\n",
|
||||||
code: 0,
|
Code: 0,
|
||||||
},
|
}, agh.ExternalCommand{
|
||||||
"networksetup -getinfo hwport": {
|
Cmd: "networksetup -getinfo hwport",
|
||||||
err: nil,
|
Err: nil,
|
||||||
out: "Manual Configuration\nIP address: 1.2.3.4\n" +
|
Out: "Manual Configuration\nIP address: 1.2.3.4\n" +
|
||||||
"Subnet mask: 255.255.255.0\nRouter: 1.2.3.1\n",
|
"Subnet mask: 255.255.255.0\nRouter: 1.2.3.1\n",
|
||||||
code: 0,
|
Code: 0,
|
||||||
},
|
}),
|
||||||
},
|
|
||||||
fsys: panicFsys,
|
fsys: panicFsys,
|
||||||
wantErrMsg: `ip address is already static`,
|
wantErrMsg: `ip address is already static`,
|
||||||
}, {
|
}, {
|
||||||
name: "reports_error",
|
name: "reports_error",
|
||||||
shell: theOnlyCmd(
|
cmdCons: agh.NewCommandConstructor(
|
||||||
"networksetup -listallhardwareports",
|
"networksetup -listallhardwareports",
|
||||||
0,
|
0,
|
||||||
"",
|
"",
|
||||||
@@ -184,18 +179,18 @@ func TestIfaceSetStaticIP(t *testing.T) {
|
|||||||
wantErrMsg: `could not find hardware port for en0`,
|
wantErrMsg: `could not find hardware port for en0`,
|
||||||
}, {
|
}, {
|
||||||
name: "resolv_conf_error",
|
name: "resolv_conf_error",
|
||||||
shell: mapShell{
|
cmdCons: agh.NewMultipleCommandConstructor(agh.ExternalCommand{
|
||||||
"networksetup -listallhardwareports": {
|
Cmd: "networksetup -listallhardwareports",
|
||||||
err: nil,
|
Err: nil,
|
||||||
out: "Hardware Port: hwport\nDevice: en0\n",
|
Out: "Hardware Port: hwport\nDevice: en0\n",
|
||||||
code: 0,
|
Code: 0,
|
||||||
},
|
}, agh.ExternalCommand{
|
||||||
"networksetup -getinfo hwport": {
|
Cmd: "networksetup -getinfo hwport",
|
||||||
err: nil,
|
Err: nil,
|
||||||
out: "IP address: 1.2.3.4\nSubnet mask: 255.255.255.0\nRouter: 1.2.3.1\n",
|
Out: "IP address: 1.2.3.4\nSubnet mask: 255.255.255.0\nRouter: 1.2.3.1\n",
|
||||||
code: 0,
|
Code: 0,
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
),
|
||||||
fsys: fstest.MapFS{
|
fsys: fstest.MapFS{
|
||||||
"etc/resolv.conf": &fstest.MapFile{
|
"etc/resolv.conf": &fstest.MapFile{
|
||||||
Data: []byte("this resolv.conf is invalid"),
|
Data: []byte("this resolv.conf is invalid"),
|
||||||
@@ -204,59 +199,57 @@ func TestIfaceSetStaticIP(t *testing.T) {
|
|||||||
wantErrMsg: `found no dns servers in etc/resolv.conf`,
|
wantErrMsg: `found no dns servers in etc/resolv.conf`,
|
||||||
}, {
|
}, {
|
||||||
name: "set_dns_error",
|
name: "set_dns_error",
|
||||||
shell: mapShell{
|
cmdCons: agh.NewMultipleCommandConstructor(agh.ExternalCommand{
|
||||||
"networksetup -listallhardwareports": {
|
Cmd: "networksetup -listallhardwareports",
|
||||||
err: nil,
|
Err: nil,
|
||||||
out: "Hardware Port: hwport\nDevice: en0\n",
|
Out: "Hardware Port: hwport\nDevice: en0\n",
|
||||||
code: 0,
|
Code: 0,
|
||||||
},
|
}, agh.ExternalCommand{
|
||||||
"networksetup -getinfo hwport": {
|
Cmd: "networksetup -getinfo hwport",
|
||||||
err: nil,
|
Err: nil,
|
||||||
out: "IP address: 1.2.3.4\nSubnet mask: 255.255.255.0\nRouter: 1.2.3.1\n",
|
Out: "IP address: 1.2.3.4\nSubnet mask: 255.255.255.0\nRouter: 1.2.3.1\n",
|
||||||
code: 0,
|
Code: 0,
|
||||||
},
|
}, agh.ExternalCommand{
|
||||||
"networksetup -setdnsservers hwport 1.1.1.1": {
|
Cmd: "networksetup -setdnsservers hwport 1.1.1.1",
|
||||||
err: errors.Error("can't set"),
|
Err: errors.Error("can't set"),
|
||||||
out: "",
|
Out: "",
|
||||||
code: 0,
|
Code: 0,
|
||||||
},
|
}),
|
||||||
},
|
|
||||||
fsys: succFsys,
|
fsys: succFsys,
|
||||||
wantErrMsg: `can't set`,
|
wantErrMsg: `can't set`,
|
||||||
}, {
|
}, {
|
||||||
name: "set_manual_error",
|
name: "set_manual_error",
|
||||||
shell: mapShell{
|
cmdCons: agh.NewMultipleCommandConstructor(agh.ExternalCommand{
|
||||||
"networksetup -listallhardwareports": {
|
Cmd: "networksetup -listallhardwareports",
|
||||||
err: nil,
|
Err: nil,
|
||||||
out: "Hardware Port: hwport\nDevice: en0\n",
|
Out: "Hardware Port: hwport\nDevice: en0\n",
|
||||||
code: 0,
|
Code: 0,
|
||||||
},
|
}, agh.ExternalCommand{
|
||||||
"networksetup -getinfo hwport": {
|
Cmd: "networksetup -getinfo hwport",
|
||||||
err: nil,
|
Err: nil,
|
||||||
out: "IP address: 1.2.3.4\nSubnet mask: 255.255.255.0\nRouter: 1.2.3.1\n",
|
Out: "IP address: 1.2.3.4\nSubnet mask: 255.255.255.0\nRouter: 1.2.3.1\n",
|
||||||
code: 0,
|
Code: 0,
|
||||||
},
|
}, agh.ExternalCommand{
|
||||||
"networksetup -setdnsservers hwport 1.1.1.1": {
|
Cmd: "networksetup -setdnsservers hwport 1.1.1.1",
|
||||||
err: nil,
|
Err: nil,
|
||||||
out: "",
|
Out: "",
|
||||||
code: 0,
|
Code: 0,
|
||||||
},
|
}, agh.ExternalCommand{
|
||||||
"networksetup -setmanual hwport 1.2.3.4 255.255.255.0 1.2.3.1": {
|
Cmd: "networksetup -setmanual hwport 1.2.3.4 255.255.255.0 1.2.3.1",
|
||||||
err: errors.Error("can't set"),
|
Err: errors.Error("can't set"),
|
||||||
out: "",
|
Out: "",
|
||||||
code: 0,
|
Code: 0,
|
||||||
},
|
}),
|
||||||
},
|
|
||||||
fsys: succFsys,
|
fsys: succFsys,
|
||||||
wantErrMsg: `can't set`,
|
wantErrMsg: `can't set`,
|
||||||
}}
|
}}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
substShell(t, tc.shell.RunCmd)
|
|
||||||
substRootDirFS(t, tc.fsys)
|
substRootDirFS(t, tc.fsys)
|
||||||
|
|
||||||
err := IfaceSetStaticIP("en0")
|
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||||
|
err := IfaceSetStaticIP(ctx, tc.cmdCons, "en0")
|
||||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,21 @@ package aghnet
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
|
"github.com/AdguardTeam/golibs/osutil/executil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ifaceHasStaticIP(ifaceName string) (ok bool, err error) {
|
func ifaceHasStaticIP(
|
||||||
|
_ context.Context,
|
||||||
|
_ executil.CommandConstructor,
|
||||||
|
ifaceName string,
|
||||||
|
) (ok bool, err error) {
|
||||||
const rcConfFilename = "etc/rc.conf"
|
const rcConfFilename = "etc/rc.conf"
|
||||||
|
|
||||||
walker := aghos.FileWalker(interfaceName(ifaceName).rcConfStaticConfig)
|
walker := aghos.FileWalker(interfaceName(ifaceName).rcConfStaticConfig)
|
||||||
@@ -52,6 +58,6 @@ func (n interfaceName) rcConfStaticConfig(r io.Reader) (_ []string, cont bool, e
|
|||||||
return nil, true, s.Err()
|
return nil, true, s.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func ifaceSetStaticIP(string) (err error) {
|
func ifaceSetStaticIP(_ context.Context, _ executil.CommandConstructor, _ string) (err error) {
|
||||||
return aghos.Unsupported("setting static ip")
|
return aghos.Unsupported("setting static ip")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"testing/fstest"
|
"testing/fstest"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/osutil/executil"
|
||||||
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -67,7 +69,8 @@ func TestIfaceHasStaticIP(t *testing.T) {
|
|||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
substRootDirFS(t, tc.rootFsys)
|
substRootDirFS(t, tc.rootFsys)
|
||||||
|
|
||||||
has, err := IfaceHasStaticIP(ifaceName)
|
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||||
|
has, err := IfaceHasStaticIP(ctx, executil.EmptyCommandConstructor{}, ifaceName)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
tc.wantHas(t, has)
|
tc.wantHas(t, has)
|
||||||
|
|||||||
@@ -3,20 +3,24 @@ package aghnet
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/agh"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
|
"github.com/AdguardTeam/golibs/osutil/executil"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// testTimeout is the common timeout for tests.
|
||||||
|
const testTimeout = 1 * time.Second
|
||||||
|
|
||||||
// substRootDirFS replaces the aghos.RootDirFS function used throughout the
|
// substRootDirFS replaces the aghos.RootDirFS function used throughout the
|
||||||
// package with fsys for tests ran under t.
|
// package with fsys for tests ran under t.
|
||||||
func substRootDirFS(t testing.TB, fsys fs.FS) {
|
func substRootDirFS(t testing.TB, fsys fs.FS) {
|
||||||
@@ -30,43 +34,6 @@ func substRootDirFS(t testing.TB, fsys fs.FS) {
|
|||||||
// RunCmdFunc is the signature of aghos.RunCommand function.
|
// RunCmdFunc is the signature of aghos.RunCommand function.
|
||||||
type RunCmdFunc func(cmd string, args ...string) (code int, out []byte, err error)
|
type RunCmdFunc func(cmd string, args ...string) (code int, out []byte, err error)
|
||||||
|
|
||||||
// substShell replaces the the aghos.RunCommand function used throughout the
|
|
||||||
// package with rc for tests ran under t.
|
|
||||||
func substShell(t testing.TB, rc RunCmdFunc) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
prev := aghosRunCommand
|
|
||||||
t.Cleanup(func() { aghosRunCommand = prev })
|
|
||||||
aghosRunCommand = rc
|
|
||||||
}
|
|
||||||
|
|
||||||
// mapShell is a substitution of aghos.RunCommand that maps the command to it's
|
|
||||||
// execution result. It's only needed to simplify testing.
|
|
||||||
//
|
|
||||||
// TODO(e.burkov): Perhaps put all the shell interactions behind an interface.
|
|
||||||
type mapShell map[string]struct {
|
|
||||||
err error
|
|
||||||
out string
|
|
||||||
code int
|
|
||||||
}
|
|
||||||
|
|
||||||
// theOnlyCmd returns mapShell that only handles a single command and arguments
|
|
||||||
// combination from cmd.
|
|
||||||
func theOnlyCmd(cmd string, code int, out string, err error) (s mapShell) {
|
|
||||||
return mapShell{cmd: {code: code, out: out, err: err}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RunCmd is a RunCmdFunc handled by s.
|
|
||||||
func (s mapShell) RunCmd(cmd string, args ...string) (code int, out []byte, err error) {
|
|
||||||
key := strings.Join(append([]string{cmd}, args...), " ")
|
|
||||||
ret, ok := s[key]
|
|
||||||
if !ok {
|
|
||||||
return 0, nil, fmt.Errorf("unexpected shell command %q", key)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret.code, []byte(ret.out), ret.err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ifaceAddrsFunc is the signature of net.InterfaceAddrs function.
|
// ifaceAddrsFunc is the signature of net.InterfaceAddrs function.
|
||||||
type ifaceAddrsFunc func() (ifaces []net.Addr, err error)
|
type ifaceAddrsFunc func() (ifaces []net.Addr, err error)
|
||||||
|
|
||||||
@@ -85,36 +52,35 @@ func TestGatewayIP(t *testing.T) {
|
|||||||
const cmd = "ip route show dev " + ifaceName
|
const cmd = "ip route show dev " + ifaceName
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
shell mapShell
|
cmdCons executil.CommandConstructor
|
||||||
want netip.Addr
|
want netip.Addr
|
||||||
name string
|
name string
|
||||||
}{{
|
}{{
|
||||||
shell: theOnlyCmd(cmd, 0, `default via 1.2.3.4 onlink`, nil),
|
cmdCons: agh.NewCommandConstructor(cmd, 0, `default via 1.2.3.4 onlink`, nil),
|
||||||
want: netip.MustParseAddr("1.2.3.4"),
|
want: netip.MustParseAddr("1.2.3.4"),
|
||||||
name: "success_v4",
|
name: "success_v4",
|
||||||
}, {
|
}, {
|
||||||
shell: theOnlyCmd(cmd, 0, `default via ::ffff onlink`, nil),
|
cmdCons: agh.NewCommandConstructor(cmd, 0, `default via ::ffff onlink`, nil),
|
||||||
want: netip.MustParseAddr("::ffff"),
|
want: netip.MustParseAddr("::ffff"),
|
||||||
name: "success_v6",
|
name: "success_v6",
|
||||||
}, {
|
}, {
|
||||||
shell: theOnlyCmd(cmd, 0, `non-default via 1.2.3.4 onlink`, nil),
|
cmdCons: agh.NewCommandConstructor(cmd, 0, `non-default via 1.2.3.4 onlink`, nil),
|
||||||
want: netip.Addr{},
|
want: netip.Addr{},
|
||||||
name: "bad_output",
|
name: "bad_output",
|
||||||
}, {
|
}, {
|
||||||
shell: theOnlyCmd(cmd, 0, "", errors.Error("can't run command")),
|
cmdCons: agh.NewCommandConstructor(cmd, 0, "", errors.Error("can't run command")),
|
||||||
want: netip.Addr{},
|
want: netip.Addr{},
|
||||||
name: "err_runcmd",
|
name: "err_runcmd",
|
||||||
}, {
|
}, {
|
||||||
shell: theOnlyCmd(cmd, 1, "", nil),
|
cmdCons: agh.NewCommandConstructor(cmd, 1, "", nil),
|
||||||
want: netip.Addr{},
|
want: netip.Addr{},
|
||||||
name: "bad_code",
|
name: "bad_code",
|
||||||
}}
|
}}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
substShell(t, tc.shell.RunCmd)
|
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||||
|
assert.Equal(t, tc.want, GatewayIP(ctx, tc.cmdCons, ifaceName))
|
||||||
assert.Equal(t, tc.want, GatewayIP(ifaceName))
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package aghnet
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
@@ -13,6 +14,7 @@ import (
|
|||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
"github.com/AdguardTeam/golibs/osutil/executil"
|
||||||
"github.com/AdguardTeam/golibs/stringutil"
|
"github.com/AdguardTeam/golibs/stringutil"
|
||||||
"github.com/google/renameio/v2/maybe"
|
"github.com/google/renameio/v2/maybe"
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
@@ -104,7 +106,11 @@ func (n interfaceName) ifacesStaticConfig(r io.Reader) (sub []string, cont bool,
|
|||||||
return sub, true, s.Err()
|
return sub, true, s.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func ifaceHasStaticIP(ifaceName string) (has bool, err error) {
|
func ifaceHasStaticIP(
|
||||||
|
_ context.Context,
|
||||||
|
_ executil.CommandConstructor,
|
||||||
|
ifaceName string,
|
||||||
|
) (has bool, err error) {
|
||||||
// TODO(a.garipov): Currently, this function returns the first definitive
|
// TODO(a.garipov): Currently, this function returns the first definitive
|
||||||
// result. So if /etc/dhcpcd.conf has and /etc/network/interfaces has no
|
// result. So if /etc/dhcpcd.conf has and /etc/network/interfaces has no
|
||||||
// static IP configuration, it will return true. Perhaps this is not the
|
// static IP configuration, it will return true. Perhaps this is not the
|
||||||
@@ -149,7 +155,11 @@ func findIfaceLine(s *bufio.Scanner, name string) (ok bool) {
|
|||||||
|
|
||||||
// ifaceSetStaticIP configures the system to retain its current IP on the
|
// ifaceSetStaticIP configures the system to retain its current IP on the
|
||||||
// interface through dhcpcd.conf.
|
// interface through dhcpcd.conf.
|
||||||
func ifaceSetStaticIP(ifaceName string) (err error) {
|
func ifaceSetStaticIP(
|
||||||
|
ctx context.Context,
|
||||||
|
cmdCons executil.CommandConstructor,
|
||||||
|
ifaceName string,
|
||||||
|
) (err error) {
|
||||||
ipNet := GetSubnet(ifaceName)
|
ipNet := GetSubnet(ifaceName)
|
||||||
if !ipNet.Addr().IsValid() {
|
if !ipNet.Addr().IsValid() {
|
||||||
return errors.Error("can't get IP address")
|
return errors.Error("can't get IP address")
|
||||||
@@ -160,7 +170,7 @@ func ifaceSetStaticIP(ifaceName string) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
gatewayIP := GatewayIP(ifaceName)
|
gatewayIP := GatewayIP(ctx, cmdCons, ifaceName)
|
||||||
add := dhcpcdConfIface(ifaceName, ipNet, gatewayIP)
|
add := dhcpcdConfIface(ifaceName, ipNet, gatewayIP)
|
||||||
|
|
||||||
body = append(body, []byte(add)...)
|
body = append(body, []byte(add)...)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"testing/fstest"
|
"testing/fstest"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/osutil/executil"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@@ -117,7 +118,8 @@ func TestHasStaticIP(t *testing.T) {
|
|||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
substRootDirFS(t, tc.rootFsys)
|
substRootDirFS(t, tc.rootFsys)
|
||||||
|
|
||||||
has, err := IfaceHasStaticIP(ifaceName)
|
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||||
|
has, err := IfaceHasStaticIP(ctx, executil.EmptyCommandConstructor{}, ifaceName)
|
||||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||||
|
|
||||||
tc.wantHas(t, has)
|
tc.wantHas(t, has)
|
||||||
|
|||||||
@@ -4,15 +4,21 @@ package aghnet
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
|
"github.com/AdguardTeam/golibs/osutil/executil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ifaceHasStaticIP(ifaceName string) (ok bool, err error) {
|
func ifaceHasStaticIP(
|
||||||
|
_ context.Context,
|
||||||
|
_ executil.CommandConstructor,
|
||||||
|
ifaceName string,
|
||||||
|
) (ok bool, err error) {
|
||||||
filename := fmt.Sprintf("etc/hostname.%s", ifaceName)
|
filename := fmt.Sprintf("etc/hostname.%s", ifaceName)
|
||||||
|
|
||||||
return aghos.FileWalker(hostnameIfStaticConfig).Walk(rootDirFS, filename)
|
return aghos.FileWalker(hostnameIfStaticConfig).Walk(rootDirFS, filename)
|
||||||
@@ -39,6 +45,6 @@ func hostnameIfStaticConfig(r io.Reader) (_ []string, ok bool, err error) {
|
|||||||
return nil, true, s.Err()
|
return nil, true, s.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func ifaceSetStaticIP(string) (err error) {
|
func ifaceSetStaticIP(_ context.Context, _ executil.CommandConstructor, _ string) (err error) {
|
||||||
return aghos.Unsupported("setting static ip")
|
return aghos.Unsupported("setting static ip")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"testing/fstest"
|
"testing/fstest"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/osutil/executil"
|
||||||
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -62,7 +64,8 @@ func TestIfaceHasStaticIP(t *testing.T) {
|
|||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
substRootDirFS(t, tc.rootFsys)
|
substRootDirFS(t, tc.rootFsys)
|
||||||
|
|
||||||
has, err := IfaceHasStaticIP(ifaceName)
|
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||||
|
has, err := IfaceHasStaticIP(ctx, executil.EmptyCommandConstructor{}, ifaceName)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
tc.wantHas(t, has)
|
tc.wantHas(t, has)
|
||||||
|
|||||||
@@ -3,12 +3,14 @@
|
|||||||
package aghnet
|
package aghnet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/AdguardTeam/golibs/osutil/executil"
|
||||||
"golang.org/x/sys/windows"
|
"golang.org/x/sys/windows"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,11 +18,15 @@ func canBindPrivilegedPorts() (can bool, err error) {
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ifaceHasStaticIP(string) (ok bool, err error) {
|
func ifaceHasStaticIP(
|
||||||
|
_ context.Context,
|
||||||
|
_ executil.CommandConstructor,
|
||||||
|
_ string,
|
||||||
|
) (ok bool, err error) {
|
||||||
return false, aghos.Unsupported("checking static ip")
|
return false, aghos.Unsupported("checking static ip")
|
||||||
}
|
}
|
||||||
|
|
||||||
func ifaceSetStaticIP(string) (err error) {
|
func ifaceSetStaticIP(_ context.Context, _ executil.CommandConstructor, _ string) (err error) {
|
||||||
return aghos.Unsupported("setting static ip")
|
return aghos.Unsupported("setting static ip")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/AdguardTeam/golibs/ioutil"
|
||||||
"github.com/AdguardTeam/golibs/osutil"
|
"github.com/AdguardTeam/golibs/osutil"
|
||||||
"github.com/AdguardTeam/golibs/osutil/executil"
|
"github.com/AdguardTeam/golibs/osutil/executil"
|
||||||
)
|
)
|
||||||
@@ -52,41 +53,43 @@ func HaveAdminRights() (bool, error) {
|
|||||||
const MaxCmdOutputSize = 64 * 1024
|
const MaxCmdOutputSize = 64 * 1024
|
||||||
|
|
||||||
// RunCommand runs shell command.
|
// RunCommand runs shell command.
|
||||||
func RunCommand(command string, arguments ...string) (code int, output []byte, err error) {
|
//
|
||||||
// TODO(s.chzhen): Pass context.
|
// TODO(s.chzhen): Consider removing this after addressing the current behavior
|
||||||
ctx := context.TODO()
|
// where a non-zero exit code is returned together with a nil error.
|
||||||
|
func RunCommand(
|
||||||
|
ctx context.Context,
|
||||||
|
cmdCons executil.CommandConstructor,
|
||||||
|
command string,
|
||||||
|
arguments ...string,
|
||||||
|
) (code int, output []byte, err error) {
|
||||||
stdoutBuf := bytes.Buffer{}
|
stdoutBuf := bytes.Buffer{}
|
||||||
stderrBuf := bytes.Buffer{}
|
stderrBuf := bytes.Buffer{}
|
||||||
|
|
||||||
err = executil.Run(
|
err = executil.Run(
|
||||||
ctx,
|
ctx,
|
||||||
executil.SystemCommandConstructor{},
|
cmdCons,
|
||||||
&executil.CommandConfig{
|
&executil.CommandConfig{
|
||||||
Path: command,
|
Path: command,
|
||||||
Args: arguments,
|
Args: arguments,
|
||||||
Stdout: &stdoutBuf,
|
Stdout: ioutil.NewTruncatedWriter(&stdoutBuf, MaxCmdOutputSize),
|
||||||
Stderr: &stderrBuf,
|
Stderr: &stderrBuf,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
out := stdoutBuf.Bytes()
|
if err == nil {
|
||||||
if len(out) > MaxCmdOutputSize {
|
return osutil.ExitCodeSuccess, stdoutBuf.Bytes(), nil
|
||||||
out = out[:MaxCmdOutputSize]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
code, ok := executil.ExitCodeFromError(err)
|
code, ok := executil.ExitCodeFromError(err)
|
||||||
if err != nil {
|
|
||||||
if ok {
|
if ok {
|
||||||
|
// Mirror the old behavior and return a nil-error on non-zero code
|
||||||
|
// status.
|
||||||
return code, stderrBuf.Bytes(), nil
|
return code, stderrBuf.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return osutil.ExitCodeFailure,
|
return osutil.ExitCodeFailure,
|
||||||
nil,
|
nil,
|
||||||
fmt.Errorf("command %q failed: %w: %s", command, err, out)
|
fmt.Errorf("command %q failed: %w: %s", command, err, stdoutBuf.Bytes())
|
||||||
}
|
|
||||||
|
|
||||||
return code, out, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PIDByCommand searches for process named command and returns its PID ignoring
|
// PIDByCommand searches for process named command and returns its PID ignoring
|
||||||
@@ -102,6 +105,8 @@ func PIDByCommand(
|
|||||||
|
|
||||||
psArgs := []string{"-A", "-o", "pid=", "-o", "comm="}
|
psArgs := []string{"-A", "-o", "pid=", "-o", "comm="}
|
||||||
|
|
||||||
|
l.DebugContext(ctx, "executing", "cmd", psCmd, "args", psArgs)
|
||||||
|
|
||||||
// Don't use -C flag here since it's a feature of linux's ps
|
// Don't use -C flag here since it's a feature of linux's ps
|
||||||
// implementation. Use POSIX-compatible flags instead.
|
// implementation. Use POSIX-compatible flags instead.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package arpdb
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
@@ -11,18 +12,15 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
"github.com/AdguardTeam/golibs/osutil"
|
"github.com/AdguardTeam/golibs/osutil"
|
||||||
|
"github.com/AdguardTeam/golibs/osutil/executil"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Variables and functions to substitute in tests.
|
// Variables and functions to substitute in tests.
|
||||||
var (
|
var (
|
||||||
// aghosRunCommand is the function to run shell commands.
|
|
||||||
aghosRunCommand = aghos.RunCommand
|
|
||||||
|
|
||||||
// rootDirFS is the filesystem pointing to the root directory.
|
// rootDirFS is the filesystem pointing to the root directory.
|
||||||
rootDirFS = osutil.RootDirFS()
|
rootDirFS = osutil.RootDirFS()
|
||||||
)
|
)
|
||||||
@@ -40,7 +38,7 @@ type Interface interface {
|
|||||||
|
|
||||||
// New returns the [Interface] properly initialized for the OS.
|
// New returns the [Interface] properly initialized for the OS.
|
||||||
func New(logger *slog.Logger) (arp Interface) {
|
func New(logger *slog.Logger) (arp Interface) {
|
||||||
return newARPDB(logger)
|
return newARPDB(logger, executil.SystemCommandConstructor{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty is the [Interface] implementation that does nothing.
|
// Empty is the [Interface] implementation that does nothing.
|
||||||
@@ -165,6 +163,7 @@ type parseNeighsFunc func(logger *slog.Logger, sc *bufio.Scanner, lenHint int) (
|
|||||||
// retrieve data.
|
// retrieve data.
|
||||||
type cmdARPDB struct {
|
type cmdARPDB struct {
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
|
cmdCons executil.CommandConstructor
|
||||||
parse parseNeighsFunc
|
parse parseNeighsFunc
|
||||||
ns *neighs
|
ns *neighs
|
||||||
cmd string
|
cmd string
|
||||||
@@ -178,14 +177,26 @@ var _ Interface = (*cmdARPDB)(nil)
|
|||||||
func (arp *cmdARPDB) Refresh() (err error) {
|
func (arp *cmdARPDB) Refresh() (err error) {
|
||||||
defer func() { err = errors.Annotate(err, "cmd arpdb: %w") }()
|
defer func() { err = errors.Annotate(err, "cmd arpdb: %w") }()
|
||||||
|
|
||||||
code, out, err := aghosRunCommand(arp.cmd, arp.args...)
|
var stdout bytes.Buffer
|
||||||
|
err = executil.Run(
|
||||||
|
// TODO(s.chzhen): Pass context.
|
||||||
|
context.TODO(),
|
||||||
|
arp.cmdCons,
|
||||||
|
&executil.CommandConfig{
|
||||||
|
Path: arp.cmd,
|
||||||
|
Args: arp.args,
|
||||||
|
Stdout: &stdout,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
code, _ := executil.ExitCodeFromError(err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("running command: %w", err)
|
return fmt.Errorf("running command: %w", err)
|
||||||
} else if code != 0 {
|
} else if code != 0 {
|
||||||
return fmt.Errorf("running command: unexpected exit code %d", code)
|
return fmt.Errorf("running command: unexpected exit code %d", code)
|
||||||
}
|
}
|
||||||
|
|
||||||
sc := bufio.NewScanner(bytes.NewReader(out))
|
sc := bufio.NewScanner(bytes.NewReader(stdout.Bytes()))
|
||||||
ns := arp.parse(arp.logger, sc, arp.ns.len())
|
ns := arp.parse(arp.logger, sc, arp.ns.len())
|
||||||
if err = sc.Err(); err != nil {
|
if err = sc.Err(); err != nil {
|
||||||
// TODO(e.burkov): This error seems unreachable. Investigate.
|
// TODO(e.burkov): This error seems unreachable. Investigate.
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
|
"github.com/AdguardTeam/golibs/osutil/executil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newARPDB(logger *slog.Logger) (arp *cmdARPDB) {
|
func newARPDB(logger *slog.Logger, cmdCons executil.CommandConstructor) (arp *cmdARPDB) {
|
||||||
return &cmdARPDB{
|
return &cmdARPDB{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
cmdCons: cmdCons,
|
||||||
parse: parseArpA,
|
parse: parseArpA,
|
||||||
ns: &neighs{
|
ns: &neighs{
|
||||||
mu: &sync.RWMutex{},
|
mu: &sync.RWMutex{},
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
package arpdb
|
package arpdb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/agh"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
@@ -23,43 +22,6 @@ var testdata fs.FS = os.DirFS("./testdata")
|
|||||||
// RunCmdFunc is the signature of aghos.RunCommand function.
|
// RunCmdFunc is the signature of aghos.RunCommand function.
|
||||||
type RunCmdFunc func(cmd string, args ...string) (code int, out []byte, err error)
|
type RunCmdFunc func(cmd string, args ...string) (code int, out []byte, err error)
|
||||||
|
|
||||||
// substShell replaces the the aghos.RunCommand function used throughout the
|
|
||||||
// package with rc for tests ran under t.
|
|
||||||
func substShell(t testing.TB, rc RunCmdFunc) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
prev := aghosRunCommand
|
|
||||||
t.Cleanup(func() { aghosRunCommand = prev })
|
|
||||||
aghosRunCommand = rc
|
|
||||||
}
|
|
||||||
|
|
||||||
// mapShell is a substitution of aghos.RunCommand that maps the command to it's
|
|
||||||
// execution result. It's only needed to simplify testing.
|
|
||||||
//
|
|
||||||
// TODO(e.burkov): Perhaps put all the shell interactions behind an interface.
|
|
||||||
type mapShell map[string]struct {
|
|
||||||
err error
|
|
||||||
out string
|
|
||||||
code int
|
|
||||||
}
|
|
||||||
|
|
||||||
// theOnlyCmd returns mapShell that only handles a single command and arguments
|
|
||||||
// combination from cmd.
|
|
||||||
func theOnlyCmd(cmd string, code int, out string, err error) (s mapShell) {
|
|
||||||
return mapShell{cmd: {code: code, out: out, err: err}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RunCmd is a RunCmdFunc handled by s.
|
|
||||||
func (s mapShell) RunCmd(cmd string, args ...string) (code int, out []byte, err error) {
|
|
||||||
key := strings.Join(append([]string{cmd}, args...), " ")
|
|
||||||
ret, ok := s[key]
|
|
||||||
if !ok {
|
|
||||||
return 0, nil, fmt.Errorf("unexpected shell command %q", key)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret.code, []byte(ret.out), ret.err
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_New(t *testing.T) {
|
func Test_New(t *testing.T) {
|
||||||
var a Interface
|
var a Interface
|
||||||
require.NotPanics(t, func() { a = New(slogutil.NewDiscardLogger()) })
|
require.NotPanics(t, func() { a = New(slogutil.NewDiscardLogger()) })
|
||||||
@@ -212,8 +174,7 @@ func TestCmdARPDB_arpa(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
t.Run("arp_a", func(t *testing.T) {
|
t.Run("arp_a", func(t *testing.T) {
|
||||||
sh := theOnlyCmd("cmd", 0, arpAOutput, nil)
|
a.cmdCons = agh.NewCommandConstructor("cmd", 0, arpAOutput, nil)
|
||||||
substShell(t, sh.RunCmd)
|
|
||||||
|
|
||||||
err := a.Refresh()
|
err := a.Refresh()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -222,24 +183,25 @@ func TestCmdARPDB_arpa(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("runcmd_error", func(t *testing.T) {
|
t.Run("runcmd_error", func(t *testing.T) {
|
||||||
sh := theOnlyCmd("cmd", 0, "", errors.Error("can't run"))
|
a.cmdCons = agh.NewCommandConstructor("cmd", 0, "", errors.Error("can't run"))
|
||||||
substShell(t, sh.RunCmd)
|
|
||||||
|
|
||||||
err := a.Refresh()
|
err := a.Refresh()
|
||||||
testutil.AssertErrorMsg(t, "cmd arpdb: running command: can't run", err)
|
testutil.AssertErrorMsg(t, "cmd arpdb: running command: running: can't run", err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("bad_code", func(t *testing.T) {
|
t.Run("bad_code", func(t *testing.T) {
|
||||||
sh := theOnlyCmd("cmd", 1, "", nil)
|
a.cmdCons = agh.NewCommandConstructor("cmd", 1, "", nil)
|
||||||
substShell(t, sh.RunCmd)
|
|
||||||
|
|
||||||
err := a.Refresh()
|
err := a.Refresh()
|
||||||
testutil.AssertErrorMsg(t, "cmd arpdb: running command: unexpected exit code 1", err)
|
testutil.AssertErrorMsg(
|
||||||
|
t,
|
||||||
|
"cmd arpdb: running command: running: exit code 1",
|
||||||
|
err,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("empty", func(t *testing.T) {
|
t.Run("empty", func(t *testing.T) {
|
||||||
sh := theOnlyCmd("cmd", 0, "", nil)
|
a.cmdCons = agh.NewCommandConstructor("cmd", 0, "", nil)
|
||||||
substShell(t, sh.RunCmd)
|
|
||||||
|
|
||||||
err := a.Refresh()
|
err := a.Refresh()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -14,10 +14,11 @@ import (
|
|||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
|
"github.com/AdguardTeam/golibs/osutil/executil"
|
||||||
"github.com/AdguardTeam/golibs/stringutil"
|
"github.com/AdguardTeam/golibs/stringutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newARPDB(logger *slog.Logger) (arp *arpdbs) {
|
func newARPDB(logger *slog.Logger, cmdCons executil.CommandConstructor) (arp *arpdbs) {
|
||||||
// Use the common storage among the implementations.
|
// Use the common storage among the implementations.
|
||||||
ns := &neighs{
|
ns := &neighs{
|
||||||
mu: &sync.RWMutex{},
|
mu: &sync.RWMutex{},
|
||||||
@@ -41,6 +42,7 @@ func newARPDB(logger *slog.Logger) (arp *arpdbs) {
|
|||||||
// Then, try "arp -a -n".
|
// Then, try "arp -a -n".
|
||||||
&cmdARPDB{
|
&cmdARPDB{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
cmdCons: cmdCons,
|
||||||
parse: parseF,
|
parse: parseF,
|
||||||
ns: ns,
|
ns: ns,
|
||||||
cmd: "arp",
|
cmd: "arp",
|
||||||
@@ -54,6 +56,7 @@ func newARPDB(logger *slog.Logger) (arp *arpdbs) {
|
|||||||
// Finally, try "ip neigh".
|
// Finally, try "ip neigh".
|
||||||
&cmdARPDB{
|
&cmdARPDB{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
cmdCons: cmdCons,
|
||||||
parse: parseIPNeigh,
|
parse: parseIPNeigh,
|
||||||
ns: ns,
|
ns: ns,
|
||||||
cmd: "ip",
|
cmd: "ip",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"testing/fstest"
|
"testing/fstest"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/agh"
|
||||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -62,15 +63,10 @@ func TestFSysARPDB(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCmdARPDB_linux(t *testing.T) {
|
func TestCmdARPDB_linux(t *testing.T) {
|
||||||
sh := mapShell{
|
|
||||||
"arp -a": {err: nil, out: arpAOutputWrt, code: 0},
|
|
||||||
"ip neigh": {err: nil, out: ipNeighOutput, code: 0},
|
|
||||||
}
|
|
||||||
substShell(t, sh.RunCmd)
|
|
||||||
|
|
||||||
t.Run("wrt", func(t *testing.T) {
|
t.Run("wrt", func(t *testing.T) {
|
||||||
a := &cmdARPDB{
|
a := &cmdARPDB{
|
||||||
logger: slogutil.NewDiscardLogger(),
|
logger: slogutil.NewDiscardLogger(),
|
||||||
|
cmdCons: agh.NewCommandConstructor("arp -a", 0, arpAOutputWrt, nil),
|
||||||
parse: parseArpAWrt,
|
parse: parseArpAWrt,
|
||||||
cmd: "arp",
|
cmd: "arp",
|
||||||
args: []string{"-a"},
|
args: []string{"-a"},
|
||||||
@@ -89,6 +85,7 @@ func TestCmdARPDB_linux(t *testing.T) {
|
|||||||
t.Run("ip_neigh", func(t *testing.T) {
|
t.Run("ip_neigh", func(t *testing.T) {
|
||||||
a := &cmdARPDB{
|
a := &cmdARPDB{
|
||||||
logger: slogutil.NewDiscardLogger(),
|
logger: slogutil.NewDiscardLogger(),
|
||||||
|
cmdCons: agh.NewCommandConstructor("ip neigh", 0, ipNeighOutput, nil),
|
||||||
parse: parseIPNeigh,
|
parse: parseIPNeigh,
|
||||||
cmd: "ip",
|
cmd: "ip",
|
||||||
args: []string{"neigh"},
|
args: []string{"neigh"},
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
|
"github.com/AdguardTeam/golibs/osutil/executil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newARPDB(logger *slog.Logger) (arp *cmdARPDB) {
|
func newARPDB(logger *slog.Logger, cmdCons executil.CommandConstructor) (arp *cmdARPDB) {
|
||||||
return &cmdARPDB{
|
return &cmdARPDB{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
cmdCons: cmdCons,
|
||||||
parse: parseArpA,
|
parse: parseArpA,
|
||||||
ns: &neighs{
|
ns: &neighs{
|
||||||
mu: &sync.RWMutex{},
|
mu: &sync.RWMutex{},
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
|
"github.com/AdguardTeam/golibs/osutil/executil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newARPDB(logger *slog.Logger) (arp *cmdARPDB) {
|
func newARPDB(logger *slog.Logger, cmdCons executil.CommandConstructor) (arp *cmdARPDB) {
|
||||||
return &cmdARPDB{
|
return &cmdARPDB{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
cmdCons: cmdCons,
|
||||||
parse: parseArpA,
|
parse: parseArpA,
|
||||||
ns: &neighs{
|
ns: &neighs{
|
||||||
mu: &sync.RWMutex{},
|
mu: &sync.RWMutex{},
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
package dhcpd
|
package dhcpd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -20,6 +21,7 @@ import (
|
|||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
|
"github.com/AdguardTeam/golibs/osutil/executil"
|
||||||
)
|
)
|
||||||
|
|
||||||
type v4ServerConfJSON struct {
|
type v4ServerConfJSON struct {
|
||||||
@@ -173,7 +175,10 @@ func (s *server) handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func (s *server) enableDHCP(ifaceName string) (code int, err error) {
|
func (s *server) enableDHCP(ifaceName string) (code int, err error) {
|
||||||
var hasStaticIP bool
|
var hasStaticIP bool
|
||||||
hasStaticIP, err = aghnet.IfaceHasStaticIP(ifaceName)
|
// TODO(s.chzhen): Pass context.
|
||||||
|
ctx := context.TODO()
|
||||||
|
cmdCons := executil.SystemCommandConstructor{}
|
||||||
|
hasStaticIP, err = aghnet.IfaceHasStaticIP(ctx, cmdCons, ifaceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, os.ErrPermission) {
|
if errors.Is(err, os.ErrPermission) {
|
||||||
// ErrPermission may happen here on Linux systems where AdGuard Home
|
// ErrPermission may happen here on Linux systems where AdGuard Home
|
||||||
@@ -202,7 +207,7 @@ func (s *server) enableDHCP(ifaceName string) (code int, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !hasStaticIP {
|
if !hasStaticIP {
|
||||||
err = aghnet.IfaceSetStaticIP(ifaceName)
|
err = aghnet.IfaceSetStaticIP(ctx, executil.SystemCommandConstructor{}, ifaceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("setting static ip: %w", err)
|
err = fmt.Errorf("setting static ip: %w", err)
|
||||||
|
|
||||||
@@ -473,7 +478,9 @@ func newNetInterfaceJSON(iface net.Interface) (out *netInterfaceJSON, err error)
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
out.GatewayIP = aghnet.GatewayIP(iface.Name)
|
// TODO(s.chzhen): Pass context.
|
||||||
|
ctx := context.TODO()
|
||||||
|
out.GatewayIP = aghnet.GatewayIP(ctx, executil.SystemCommandConstructor{}, iface.Name)
|
||||||
|
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
@@ -558,7 +565,10 @@ func (s *server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Reque
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if isStaticIP, serr := aghnet.IfaceHasStaticIP(ifaceName); serr != nil {
|
// TODO(s.chzhen): Pass context.
|
||||||
|
ctx := context.TODO()
|
||||||
|
cmdCons := executil.SystemCommandConstructor{}
|
||||||
|
if isStaticIP, serr := aghnet.IfaceHasStaticIP(ctx, cmdCons, ifaceName); serr != nil {
|
||||||
result.V4.StaticIP.Static = "error"
|
result.V4.StaticIP.Static = "error"
|
||||||
result.V4.StaticIP.Error = serr.Error()
|
result.V4.StaticIP.Error = serr.Error()
|
||||||
} else if !isStaticIP {
|
} else if !isStaticIP {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
||||||
|
"github.com/AdguardTeam/golibs/container"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/golibs/osutil/executil"
|
"github.com/AdguardTeam/golibs/osutil/executil"
|
||||||
@@ -202,6 +203,10 @@ func (web *webAPI) handleInstallCheckConfig(w http.ResponseWriter, r *http.Reque
|
|||||||
// It either checks if we have a static IP
|
// It either checks if we have a static IP
|
||||||
// Or if set=true, it tries to set it
|
// Or if set=true, it tries to set it
|
||||||
func handleStaticIP(ip netip.Addr, set bool) staticIPJSON {
|
func handleStaticIP(ip netip.Addr, set bool) staticIPJSON {
|
||||||
|
// TODO(s.chzhen): Pass context.
|
||||||
|
ctx := context.TODO()
|
||||||
|
cmdCons := executil.SystemCommandConstructor{}
|
||||||
|
|
||||||
resp := staticIPJSON{}
|
resp := staticIPJSON{}
|
||||||
|
|
||||||
interfaceName := aghnet.InterfaceByIP(ip)
|
interfaceName := aghnet.InterfaceByIP(ip)
|
||||||
@@ -215,7 +220,7 @@ func handleStaticIP(ip netip.Addr, set bool) staticIPJSON {
|
|||||||
|
|
||||||
if set {
|
if set {
|
||||||
// Try to set static IP for the specified interface
|
// Try to set static IP for the specified interface
|
||||||
err := aghnet.IfaceSetStaticIP(interfaceName)
|
err := aghnet.IfaceSetStaticIP(ctx, cmdCons, interfaceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.Static = "error"
|
resp.Static = "error"
|
||||||
resp.Error = err.Error()
|
resp.Error = err.Error()
|
||||||
@@ -225,7 +230,7 @@ func handleStaticIP(ip netip.Addr, set bool) staticIPJSON {
|
|||||||
|
|
||||||
// Fallthrough here even if we set static IP
|
// Fallthrough here even if we set static IP
|
||||||
// Check if we have a static IP and return the details
|
// Check if we have a static IP and return the details
|
||||||
isStaticIP, err := aghnet.IfaceHasStaticIP(interfaceName)
|
isStaticIP, err := aghnet.IfaceHasStaticIP(ctx, cmdCons, interfaceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.Static = "error"
|
resp.Static = "error"
|
||||||
resp.Error = err.Error()
|
resp.Error = err.Error()
|
||||||
@@ -244,65 +249,30 @@ func checkDNSStubListener(ctx context.Context, l *slog.Logger) (ok bool) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
cmds := container.KeyValues[string, []string]{{
|
||||||
systemctlCmd = "systemctl"
|
Key: "systemctl",
|
||||||
grepCmd = "grep"
|
Value: []string{"is-enabled", "systemd-resolved"},
|
||||||
)
|
}, {
|
||||||
|
Key: "grep",
|
||||||
|
Value: []string{"-E", "#?DNSStubListener=yes", "/etc/systemd/resolved.conf"},
|
||||||
|
}}
|
||||||
|
|
||||||
var (
|
for _, cmd := range cmds {
|
||||||
systemctlArgs = []string{"is-enabled", "systemd-resolved"}
|
l.DebugContext(ctx, "executing", "cmd", cmd.Key, "args", cmd.Value)
|
||||||
systemctlStdout bytes.Buffer
|
|
||||||
systemctlStderr bytes.Buffer
|
|
||||||
|
|
||||||
grepArgs = []string{"-E", "#?DNSStubListener=yes", "/etc/systemd/resolved.conf"}
|
|
||||||
grepStdout bytes.Buffer
|
|
||||||
grepStderr bytes.Buffer
|
|
||||||
)
|
|
||||||
|
|
||||||
l.DebugContext(ctx, "executing", "cmd", systemctlCmd, "args", systemctlArgs)
|
|
||||||
|
|
||||||
err := executil.Run(
|
err := executil.Run(
|
||||||
ctx,
|
ctx,
|
||||||
executil.SystemCommandConstructor{},
|
executil.SystemCommandConstructor{},
|
||||||
&executil.CommandConfig{
|
&executil.CommandConfig{
|
||||||
Path: systemctlCmd,
|
Path: cmd.Key,
|
||||||
Args: systemctlArgs,
|
Args: cmd.Value,
|
||||||
Stdout: &systemctlStdout,
|
|
||||||
Stderr: &systemctlStderr,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.InfoContext(
|
l.InfoContext(ctx, "execution failed", "cmd", cmd.Key, slogutil.KeyError, err)
|
||||||
ctx,
|
|
||||||
"execution failed",
|
|
||||||
"cmd", systemctlCmd,
|
|
||||||
slogutil.KeyError, err,
|
|
||||||
)
|
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
l.DebugContext(ctx, "executing", "cmd", grepCmd, "args", grepArgs)
|
|
||||||
|
|
||||||
err = executil.Run(
|
|
||||||
ctx,
|
|
||||||
executil.SystemCommandConstructor{},
|
|
||||||
&executil.CommandConfig{
|
|
||||||
Path: grepCmd,
|
|
||||||
Args: grepArgs,
|
|
||||||
Stdout: &grepStdout,
|
|
||||||
Stderr: &grepStderr,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
l.InfoContext(
|
|
||||||
ctx,
|
|
||||||
"execution failed",
|
|
||||||
"cmd", grepCmd,
|
|
||||||
slogutil.KeyError, err,
|
|
||||||
)
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import (
|
|||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
"github.com/AdguardTeam/golibs/netutil/urlutil"
|
"github.com/AdguardTeam/golibs/netutil/urlutil"
|
||||||
"github.com/AdguardTeam/golibs/osutil"
|
"github.com/AdguardTeam/golibs/osutil"
|
||||||
|
"github.com/AdguardTeam/golibs/osutil/executil"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Global context
|
// Global context
|
||||||
@@ -822,6 +823,7 @@ func newUpdater(
|
|||||||
return updater.NewUpdater(&updater.Config{
|
return updater.NewUpdater(&updater.Config{
|
||||||
Client: conf.Filtering.HTTPClient,
|
Client: conf.Filtering.HTTPClient,
|
||||||
Logger: l,
|
Logger: l,
|
||||||
|
CommandConstructor: executil.SystemCommandConstructor{},
|
||||||
Version: version.Version(),
|
Version: version.Version(),
|
||||||
Channel: version.Channel(),
|
Channel: version.Channel(),
|
||||||
GOARCH: runtime.GOARCH,
|
GOARCH: runtime.GOARCH,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/golibs/netutil/urlutil"
|
"github.com/AdguardTeam/golibs/netutil/urlutil"
|
||||||
"github.com/AdguardTeam/golibs/osutil"
|
"github.com/AdguardTeam/golibs/osutil"
|
||||||
|
"github.com/AdguardTeam/golibs/osutil/executil"
|
||||||
"github.com/kardianos/service"
|
"github.com/kardianos/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -76,11 +77,11 @@ func (p *program) Stop(_ service.Service) (err error) {
|
|||||||
//
|
//
|
||||||
// On OpenWrt, the service utility may not exist. We use our service script
|
// On OpenWrt, the service utility may not exist. We use our service script
|
||||||
// directly in this case.
|
// directly in this case.
|
||||||
func svcStatus(s service.Service) (status service.Status, err error) {
|
func svcStatus(ctx context.Context, s service.Service) (status service.Status, err error) {
|
||||||
status, err = s.Status()
|
status, err = s.Status()
|
||||||
if err != nil && service.Platform() == "unix-systemv" {
|
if err != nil && service.Platform() == "unix-systemv" {
|
||||||
var code int
|
var code int
|
||||||
code, err = runInitdCommand("status")
|
code, err = runInitdCommand(ctx, "status")
|
||||||
if err != nil || code != 0 {
|
if err != nil || code != 0 {
|
||||||
return service.StatusStopped, nil
|
return service.StatusStopped, nil
|
||||||
}
|
}
|
||||||
@@ -105,7 +106,7 @@ func svcAction(ctx context.Context, l *slog.Logger, s service.Service, action st
|
|||||||
err = service.Control(s, action)
|
err = service.Control(s, action)
|
||||||
if err != nil && service.Platform() == "unix-systemv" &&
|
if err != nil && service.Platform() == "unix-systemv" &&
|
||||||
(action == "start" || action == "stop" || action == "restart") {
|
(action == "start" || action == "stop" || action == "restart") {
|
||||||
_, err = runInitdCommand(action)
|
_, err = runInitdCommand(ctx, action)
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
@@ -326,7 +327,7 @@ func handleServiceStatusCommand(
|
|||||||
l *slog.Logger,
|
l *slog.Logger,
|
||||||
s service.Service,
|
s service.Service,
|
||||||
) {
|
) {
|
||||||
status, errSt := svcStatus(s)
|
status, errSt := svcStatus(ctx, s)
|
||||||
if errSt != nil {
|
if errSt != nil {
|
||||||
l.ErrorContext(ctx, "failed to get service status", slogutil.KeyError, errSt)
|
l.ErrorContext(ctx, "failed to get service status", slogutil.KeyError, errSt)
|
||||||
os.Exit(osutil.ExitCodeFailure)
|
os.Exit(osutil.ExitCodeFailure)
|
||||||
@@ -356,7 +357,7 @@ func handleServiceInstallCommand(ctx context.Context, l *slog.Logger, s service.
|
|||||||
// On OpenWrt it is important to run enable after the service
|
// On OpenWrt it is important to run enable after the service
|
||||||
// installation. Otherwise, the service won't start on the system
|
// installation. Otherwise, the service won't start on the system
|
||||||
// startup.
|
// startup.
|
||||||
_, err = runInitdCommand("enable")
|
_, err = runInitdCommand(ctx, "enable")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.ErrorContext(ctx, "running init enable", slogutil.KeyError, err)
|
l.ErrorContext(ctx, "running init enable", slogutil.KeyError, err)
|
||||||
os.Exit(osutil.ExitCodeFailure)
|
os.Exit(osutil.ExitCodeFailure)
|
||||||
@@ -386,7 +387,7 @@ func handleServiceUninstallCommand(ctx context.Context, l *slog.Logger, s servic
|
|||||||
if aghos.IsOpenWrt() {
|
if aghos.IsOpenWrt() {
|
||||||
// On OpenWrt it is important to run disable command first
|
// On OpenWrt it is important to run disable command first
|
||||||
// as it will remove the symlink
|
// as it will remove the symlink
|
||||||
_, err := runInitdCommand("disable")
|
_, err := runInitdCommand(ctx, "disable")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.ErrorContext(ctx, "running init disable", slogutil.KeyError, err)
|
l.ErrorContext(ctx, "running init disable", slogutil.KeyError, err)
|
||||||
os.Exit(osutil.ExitCodeFailure)
|
os.Exit(osutil.ExitCodeFailure)
|
||||||
@@ -458,10 +459,11 @@ func configureService(c *service.Config) {
|
|||||||
|
|
||||||
// runInitdCommand runs init.d service command
|
// runInitdCommand runs init.d service command
|
||||||
// returns command code or error if any
|
// returns command code or error if any
|
||||||
func runInitdCommand(action string) (int, error) {
|
func runInitdCommand(ctx context.Context, action string) (int, error) {
|
||||||
confPath := "/etc/init.d/" + serviceName
|
confPath := "/etc/init.d/" + serviceName
|
||||||
// Pass the script and action as a single string argument.
|
// Pass the script and action as a single string argument.
|
||||||
code, _, err := aghos.RunCommand("sh", "-c", confPath+" "+action)
|
cmdCons := executil.SystemCommandConstructor{}
|
||||||
|
code, _, err := aghos.RunCommand(ctx, cmdCons, "sh", "-c", confPath+" "+action)
|
||||||
|
|
||||||
return code, err
|
return code, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,7 +87,9 @@ func (svc *sysvService) Install() (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _, err = aghos.RunCommand("update-rc.d", svc.name, "defaults")
|
cmdCons := executil.SystemCommandConstructor{}
|
||||||
|
// TODO(s.chzhen): Pass context.
|
||||||
|
_, _, err = aghos.RunCommand(context.TODO(), cmdCons, "update-rc.d", svc.name, "defaults")
|
||||||
|
|
||||||
// Don't wrap an error since it's informative enough as is.
|
// Don't wrap an error since it's informative enough as is.
|
||||||
return err
|
return err
|
||||||
@@ -102,7 +104,9 @@ func (svc *sysvService) Uninstall() (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _, err = aghos.RunCommand("update-rc.d", svc.name, "remove")
|
cmdCons := executil.SystemCommandConstructor{}
|
||||||
|
// TODO(s.chzhen): Pass context.
|
||||||
|
_, _, err = aghos.RunCommand(context.TODO(), cmdCons, "update-rc.d", svc.name, "remove")
|
||||||
|
|
||||||
// Don't wrap an error since it's informative enough as is.
|
// Don't wrap an error since it's informative enough as is.
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package home
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"cmp"
|
"cmp"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
@@ -15,6 +16,7 @@ import (
|
|||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
"github.com/AdguardTeam/golibs/osutil/executil"
|
||||||
"github.com/kardianos/service"
|
"github.com/kardianos/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -210,8 +212,11 @@ func (s *openbsdRunComService) configureSysStartup(enable bool) (err error) {
|
|||||||
cmd = "disable"
|
cmd = "disable"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(s.chzhen): Pass context.
|
||||||
|
ctx := context.TODO()
|
||||||
|
cmdCons := executil.SystemCommandConstructor{}
|
||||||
var code int
|
var code int
|
||||||
code, _, err = aghos.RunCommand("rcctl", cmd, s.cfg.Name)
|
code, _, err = aghos.RunCommand(ctx, cmdCons, "rcctl", cmd, s.cfg.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if code != 0 {
|
} else if code != 0 {
|
||||||
@@ -312,11 +317,15 @@ func (s *openbsdRunComService) runCom(cmd string) (out string, err error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(s.chzhen): Pass context.
|
||||||
|
ctx := context.TODO()
|
||||||
|
cmdCons := executil.SystemCommandConstructor{}
|
||||||
|
|
||||||
// TODO(e.burkov): It's possible that os.ErrNotExist is caused by
|
// TODO(e.burkov): It's possible that os.ErrNotExist is caused by
|
||||||
// something different than the service script's non-existence. Keep it
|
// something different than the service script's non-existence. Keep it
|
||||||
// in mind, when replace the aghos.RunCommand.
|
// in mind, when replace the aghos.RunCommand.
|
||||||
var outData []byte
|
var outData []byte
|
||||||
_, outData, err = aghos.RunCommand(scriptPath, cmd)
|
_, outData, err = aghos.RunCommand(ctx, cmdCons, scriptPath, cmd)
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
return "", service.ErrNotInstalled
|
return "", service.ErrNotInstalled
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/updater"
|
"github.com/AdguardTeam/AdGuardHome/internal/updater"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
||||||
|
"github.com/AdguardTeam/golibs/osutil/executil"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -60,6 +61,7 @@ func TestUpdater_VersionInfo(t *testing.T) {
|
|||||||
u := updater.NewUpdater(&updater.Config{
|
u := updater.NewUpdater(&updater.Config{
|
||||||
Client: srv.Client(),
|
Client: srv.Client(),
|
||||||
Logger: testLogger,
|
Logger: testLogger,
|
||||||
|
CommandConstructor: executil.EmptyCommandConstructor{},
|
||||||
Version: "v0.103.0-beta.1",
|
Version: "v0.103.0-beta.1",
|
||||||
Channel: version.ChannelBeta,
|
Channel: version.ChannelBeta,
|
||||||
GOARCH: "arm",
|
GOARCH: "arm",
|
||||||
@@ -134,6 +136,7 @@ func TestUpdater_VersionInfo_others(t *testing.T) {
|
|||||||
u := updater.NewUpdater(&updater.Config{
|
u := updater.NewUpdater(&updater.Config{
|
||||||
Client: fakeClient,
|
Client: fakeClient,
|
||||||
Logger: testLogger,
|
Logger: testLogger,
|
||||||
|
CommandConstructor: executil.EmptyCommandConstructor{},
|
||||||
Version: "v0.103.0-beta.1",
|
Version: "v0.103.0-beta.1",
|
||||||
Channel: version.ChannelBeta,
|
Channel: version.ChannelBeta,
|
||||||
GOOS: "linux",
|
GOOS: "linux",
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ type Updater struct {
|
|||||||
client *http.Client
|
client *http.Client
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
|
|
||||||
|
cmdCons executil.CommandConstructor
|
||||||
|
|
||||||
version string
|
version string
|
||||||
channel string
|
channel string
|
||||||
goarch string
|
goarch string
|
||||||
@@ -89,6 +91,9 @@ type Config struct {
|
|||||||
// be nil, see [DefaultVersionURL].
|
// be nil, see [DefaultVersionURL].
|
||||||
VersionCheckURL *url.URL
|
VersionCheckURL *url.URL
|
||||||
|
|
||||||
|
// CommandConstructor is used to run external commands. It must not be nil.
|
||||||
|
CommandConstructor executil.CommandConstructor
|
||||||
|
|
||||||
// Version is the current AdGuard Home version. It must not be empty.
|
// Version is the current AdGuard Home version. It must not be empty.
|
||||||
Version string
|
Version string
|
||||||
|
|
||||||
@@ -130,6 +135,8 @@ func NewUpdater(conf *Config) *Updater {
|
|||||||
client: conf.Client,
|
client: conf.Client,
|
||||||
logger: conf.Logger,
|
logger: conf.Logger,
|
||||||
|
|
||||||
|
cmdCons: conf.CommandConstructor,
|
||||||
|
|
||||||
version: conf.Version,
|
version: conf.Version,
|
||||||
channel: conf.Channel,
|
channel: conf.Channel,
|
||||||
goarch: conf.GOARCH,
|
goarch: conf.GOARCH,
|
||||||
@@ -293,9 +300,11 @@ func (u *Updater) check(ctx context.Context) (err error) {
|
|||||||
stderr bytes.Buffer
|
stderr bytes.Buffer
|
||||||
)
|
)
|
||||||
|
|
||||||
|
u.logger.DebugContext(ctx, "executing", "cmd", u.updateExeName, "args", args)
|
||||||
|
|
||||||
err = executil.Run(
|
err = executil.Run(
|
||||||
ctx,
|
ctx,
|
||||||
executil.SystemCommandConstructor{},
|
u.cmdCons,
|
||||||
&executil.CommandConfig{
|
&executil.CommandConfig{
|
||||||
Path: u.updateExeName,
|
Path: u.updateExeName,
|
||||||
Args: args,
|
Args: args,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
|
"github.com/AdguardTeam/golibs/osutil/executil"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -60,6 +61,7 @@ func TestUpdater_internal(t *testing.T) {
|
|||||||
u := NewUpdater(&Config{
|
u := NewUpdater(&Config{
|
||||||
Client: fakeClient,
|
Client: fakeClient,
|
||||||
Logger: slogutil.NewDiscardLogger(),
|
Logger: slogutil.NewDiscardLogger(),
|
||||||
|
CommandConstructor: executil.EmptyCommandConstructor{},
|
||||||
GOOS: tc.os,
|
GOOS: tc.os,
|
||||||
Version: "v0.103.0",
|
Version: "v0.103.0",
|
||||||
ExecPath: exePath,
|
ExecPath: exePath,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/AdguardTeam/AdGuardHome/internal/updater"
|
"github.com/AdguardTeam/AdGuardHome/internal/updater"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
||||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
|
"github.com/AdguardTeam/golibs/osutil/executil"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -78,6 +79,7 @@ func TestUpdater_Update(t *testing.T) {
|
|||||||
u := updater.NewUpdater(&updater.Config{
|
u := updater.NewUpdater(&updater.Config{
|
||||||
Client: srv.Client(),
|
Client: srv.Client(),
|
||||||
Logger: testLogger,
|
Logger: testLogger,
|
||||||
|
CommandConstructor: executil.EmptyCommandConstructor{},
|
||||||
GOARCH: "amd64",
|
GOARCH: "amd64",
|
||||||
GOOS: "linux",
|
GOOS: "linux",
|
||||||
Version: "v0.103.0",
|
Version: "v0.103.0",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/golibs/osutil"
|
"github.com/AdguardTeam/golibs/osutil"
|
||||||
|
"github.com/AdguardTeam/golibs/osutil/executil"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -95,7 +96,7 @@ func main() {
|
|||||||
|
|
||||||
errors.Check(cli.upload())
|
errors.Check(cli.upload())
|
||||||
case "auto-add":
|
case "auto-add":
|
||||||
err := autoAdd(conf.LocalizableFiles[0])
|
err := autoAdd(ctx, l, conf.LocalizableFiles[0])
|
||||||
errors.Check(err)
|
errors.Check(err)
|
||||||
default:
|
default:
|
||||||
usage("unknown command")
|
usage("unknown command")
|
||||||
@@ -394,10 +395,12 @@ func findUnused(fileNames []string, loc locales) (err error) {
|
|||||||
|
|
||||||
// autoAdd adds locales with additions to the git and restores locales with
|
// autoAdd adds locales with additions to the git and restores locales with
|
||||||
// deletions.
|
// deletions.
|
||||||
func autoAdd(basePath string) (err error) {
|
func autoAdd(ctx context.Context, l *slog.Logger, basePath string) (err error) {
|
||||||
defer func() { err = errors.Annotate(err, "auto add: %w") }()
|
defer func() { err = errors.Annotate(err, "auto add: %w") }()
|
||||||
|
|
||||||
adds, dels, err := changedLocales()
|
cmdCons := executil.SystemCommandConstructor{}
|
||||||
|
|
||||||
|
adds, dels, err := changedLocales(ctx, l, cmdCons)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Don't wrap the error since it's informative enough as is.
|
// Don't wrap the error since it's informative enough as is.
|
||||||
return err
|
return err
|
||||||
@@ -407,13 +410,13 @@ func autoAdd(basePath string) (err error) {
|
|||||||
return errors.Error("base locale contains deletions")
|
return errors.Error("base locale contains deletions")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handleAdds(adds)
|
err = handleAdds(ctx, l, cmdCons, adds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Don't wrap the error since it's informative enough as is.
|
// Don't wrap the error since it's informative enough as is.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handleDels(dels)
|
err = handleDels(ctx, l, cmdCons, dels)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Don't wrap the error since it's informative enough as is.
|
// Don't wrap the error since it's informative enough as is.
|
||||||
return nil
|
return nil
|
||||||
@@ -422,14 +425,24 @@ func autoAdd(basePath string) (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// gitCmd is the shell command for Git.
|
||||||
|
const gitCmd = "git"
|
||||||
|
|
||||||
// handleAdds adds locales with additions to the git.
|
// handleAdds adds locales with additions to the git.
|
||||||
func handleAdds(locales []string) (err error) {
|
func handleAdds(
|
||||||
|
ctx context.Context,
|
||||||
|
l *slog.Logger,
|
||||||
|
cmdCons executil.CommandConstructor,
|
||||||
|
locales []string,
|
||||||
|
) (err error) {
|
||||||
if len(locales) == 0 {
|
if len(locales) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
args := append([]string{"add"}, locales...)
|
gitArgs := append([]string{"add"}, locales...)
|
||||||
code, out, err := aghos.RunCommand("git", args...)
|
l.DebugContext(ctx, "executing", "cmd", gitCmd, "args", gitArgs)
|
||||||
|
|
||||||
|
code, out, err := aghos.RunCommand(ctx, cmdCons, gitCmd, gitArgs...)
|
||||||
|
|
||||||
if err != nil || code != 0 {
|
if err != nil || code != 0 {
|
||||||
return fmt.Errorf("git add exited with code %d output %q: %w", code, out, err)
|
return fmt.Errorf("git add exited with code %d output %q: %w", code, out, err)
|
||||||
@@ -439,13 +452,20 @@ func handleAdds(locales []string) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handleDels restores locales with deletions.
|
// handleDels restores locales with deletions.
|
||||||
func handleDels(locales []string) (err error) {
|
func handleDels(
|
||||||
|
ctx context.Context,
|
||||||
|
l *slog.Logger,
|
||||||
|
cmdCons executil.CommandConstructor,
|
||||||
|
locales []string,
|
||||||
|
) (err error) {
|
||||||
if len(locales) == 0 {
|
if len(locales) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
args := append([]string{"restore"}, locales...)
|
gitArgs := append([]string{"restore"}, locales...)
|
||||||
code, out, err := aghos.RunCommand("git", args...)
|
l.DebugContext(ctx, "executing", "cmd", gitCmd, "args", gitArgs)
|
||||||
|
|
||||||
|
code, out, err := aghos.RunCommand(ctx, cmdCons, gitCmd, gitArgs...)
|
||||||
|
|
||||||
if err != nil || code != 0 {
|
if err != nil || code != 0 {
|
||||||
return fmt.Errorf("git restore exited with code %d output %q: %w", code, out, err)
|
return fmt.Errorf("git restore exited with code %d output %q: %w", code, out, err)
|
||||||
@@ -457,14 +477,17 @@ func handleDels(locales []string) (err error) {
|
|||||||
// changedLocales returns cleaned paths of locales with changes or error. adds
|
// changedLocales returns cleaned paths of locales with changes or error. adds
|
||||||
// is the list of locales with only additions. dels is the list of locales
|
// is the list of locales with only additions. dels is the list of locales
|
||||||
// with only deletions.
|
// with only deletions.
|
||||||
func changedLocales() (adds, dels []string, err error) {
|
func changedLocales(
|
||||||
|
ctx context.Context,
|
||||||
|
l *slog.Logger,
|
||||||
|
cmdCons executil.CommandConstructor,
|
||||||
|
) (adds, dels []string, err error) {
|
||||||
defer func() { err = errors.Annotate(err, "getting changes: %w") }()
|
defer func() { err = errors.Annotate(err, "getting changes: %w") }()
|
||||||
|
|
||||||
const gitCmd = "git"
|
|
||||||
|
|
||||||
gitArgs := []string{"diff", "--numstat", localesDir}
|
gitArgs := []string{"diff", "--numstat", localesDir}
|
||||||
|
l.DebugContext(ctx, "executing", "cmd", gitCmd, "args", gitArgs)
|
||||||
|
|
||||||
code, out, err := aghos.RunCommand(gitCmd, gitArgs...)
|
code, out, err := aghos.RunCommand(ctx, cmdCons, gitCmd, gitArgs...)
|
||||||
if err != nil || code != 0 {
|
if err != nil || code != 0 {
|
||||||
return nil, nil, fmt.Errorf("executing cmd: %w", err)
|
return nil, nil, fmt.Errorf("executing cmd: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user