Skip to content

Commit 4fc73dc

Browse files
committed
all: use executil
1 parent 5e2b440 commit 4fc73dc

21 files changed

+524
-270
lines changed

internal/agh/agh.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ package agh
33

44
import (
55
"context"
6+
"fmt"
7+
"strings"
8+
9+
"github.com/AdguardTeam/golibs/osutil"
10+
"github.com/AdguardTeam/golibs/osutil/executil"
11+
"github.com/AdguardTeam/golibs/testutil/fakeos/fakeexec"
612
)
713

814
// ConfigModifier defines an interface for updating the global configuration.
@@ -20,3 +26,124 @@ var _ ConfigModifier = EmptyConfigModifier{}
2026

2127
// Apply implements the [ConfigModifier] for EmptyConfigModifier.
2228
func (em EmptyConfigModifier) Apply(ctx context.Context) {}
29+
30+
// TODO(s.chzhen): !! Is there another way?
31+
//
32+
// TODO(s.chzhen): !! Docs, naming.
33+
//
34+
// TODO(s.chzhen): Move to aghtest once the import cycle is resolved.
35+
type exitErr struct {
36+
code osutil.ExitCode
37+
}
38+
39+
// type check
40+
var _ executil.ExitCodeError = exitErr{}
41+
42+
func (e exitErr) Error() (s string) {
43+
return fmt.Sprintf("exit code %d", e.code)
44+
}
45+
46+
func (e exitErr) ExitCode() (code osutil.ExitCode) {
47+
return e.code
48+
}
49+
50+
type ExternalCommand struct {
51+
Err error
52+
Cmd string
53+
Out string
54+
Code int
55+
}
56+
57+
func keyCommand(path string, args []string) (k string) {
58+
return path + " " + strings.Join(args, " ")
59+
}
60+
61+
func parseCommand(s string) (path string, args []string) {
62+
f := strings.Fields(s)
63+
if len(f) == 0 {
64+
return "", nil
65+
}
66+
67+
return f[0], f[1:]
68+
}
69+
70+
// NewMultipleCommandConstructor is a helper function that returns a mock
71+
// [executil.CommandConstructor] for tests.
72+
func NewMultipleCommandConstructor(cmds ...ExternalCommand) (cs executil.CommandConstructor) {
73+
table := make(map[string]ExternalCommand, len(cmds))
74+
for _, ec := range cmds {
75+
p, a := parseCommand(ec.Cmd)
76+
table[keyCommand(p, a)] = ec
77+
}
78+
79+
return &fakeexec.CommandConstructor{
80+
OnNew: func(
81+
_ context.Context,
82+
conf *executil.CommandConfig,
83+
) (c executil.Command, err error) {
84+
ec := table[keyCommand(conf.Path, conf.Args)]
85+
86+
cmd := fakeexec.NewCommand()
87+
cmd.OnStart = func(_ context.Context) (err error) {
88+
if ec.Out != "" {
89+
_, _ = conf.Stdout.Write([]byte(ec.Out))
90+
}
91+
92+
return nil
93+
}
94+
95+
cmd.OnWait = func(_ context.Context) (err error) {
96+
if ec.Err != nil {
97+
return ec.Err
98+
}
99+
100+
if ec.Code != 0 {
101+
return exitErr{code: ec.Code}
102+
}
103+
104+
return nil
105+
}
106+
107+
return cmd, nil
108+
},
109+
}
110+
}
111+
112+
// NewCommandConstructor is a helper function that returns a mock
113+
// [executil.CommandConstructor] for tests.
114+
func NewCommandConstructor(
115+
_ string,
116+
code int,
117+
stdout string,
118+
cmdErr error,
119+
) (cs executil.CommandConstructor) {
120+
return &fakeexec.CommandConstructor{
121+
OnNew: func(
122+
_ context.Context,
123+
conf *executil.CommandConfig,
124+
) (c executil.Command, err error) {
125+
cmd := fakeexec.NewCommand()
126+
cmd.OnStart = func(_ context.Context) (err error) {
127+
if conf.Stdout != nil {
128+
_, _ = conf.Stdout.Write([]byte(stdout))
129+
}
130+
131+
return nil
132+
}
133+
134+
cmd.OnWait = func(_ context.Context) (err error) {
135+
if cmdErr != nil {
136+
return cmdErr
137+
}
138+
139+
if code != 0 {
140+
return exitErr{code: code}
141+
}
142+
143+
return nil
144+
}
145+
146+
return cmd, nil
147+
},
148+
}
149+
}

internal/aghnet/net.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/AdguardTeam/golibs/errors"
1919
"github.com/AdguardTeam/golibs/log"
2020
"github.com/AdguardTeam/golibs/osutil"
21+
"github.com/AdguardTeam/golibs/osutil/executil"
2122
)
2223

2324
// DialContextFunc is the semantic alias for dialing functions, such as
@@ -27,7 +28,16 @@ type DialContextFunc = func(ctx context.Context, network, addr string) (conn net
2728
// Variables and functions to substitute in tests.
2829
var (
2930
// aghosRunCommand is the function to run shell commands.
30-
aghosRunCommand = aghos.RunCommand
31+
//
32+
// TODO(s.chzhen): Use [aghos.RunCommand] directly.
33+
aghosRunCommand = (func() func(string, ...string) (int, []byte, error) {
34+
ctx := context.TODO()
35+
cmdCons := executil.SystemCommandConstructor{}
36+
37+
return func(command string, arguments ...string) (int, []byte, error) {
38+
return aghos.RunCommand(ctx, cmdCons, command, arguments...)
39+
}
40+
})()
3141

3242
// netInterfaces is the function to get the available network interfaces.
3343
netInterfaceAddrs = net.InterfaceAddrs

internal/aghos/os.go

Lines changed: 58 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,23 @@ package aghos
55

66
import (
77
"bufio"
8+
"bytes"
89
"context"
910
"fmt"
1011
"io"
1112
"io/fs"
1213
"log/slog"
1314
"os"
14-
"os/exec"
1515
"path"
1616
"runtime"
1717
"slices"
1818
"strconv"
1919
"strings"
2020

2121
"github.com/AdguardTeam/golibs/errors"
22+
"github.com/AdguardTeam/golibs/ioutil"
23+
"github.com/AdguardTeam/golibs/osutil"
24+
"github.com/AdguardTeam/golibs/osutil/executil"
2225
)
2326

2427
// Default file, binary, and directory permissions.
@@ -50,23 +53,48 @@ func HaveAdminRights() (bool, error) {
5053
const MaxCmdOutputSize = 64 * 1024
5154

5255
// RunCommand runs shell command.
53-
func RunCommand(command string, arguments ...string) (code int, output []byte, err error) {
54-
cmd := exec.Command(command, arguments...)
55-
out, err := cmd.Output()
56-
57-
out = out[:min(len(out), MaxCmdOutputSize)]
58-
59-
if err != nil {
60-
if eerr := new(exec.ExitError); errors.As(err, &eerr) {
61-
return eerr.ExitCode(), eerr.Stderr, nil
62-
}
56+
//
57+
// TODO(s.chzhen): Consider removing this after addressing the current behavior
58+
// where a non-zero exit code is returned together with a nil error.
59+
func RunCommand(
60+
ctx context.Context,
61+
cmdCons executil.CommandConstructor,
62+
command string,
63+
arguments ...string,
64+
) (code int, output []byte, err error) {
65+
stdoutBuf := bytes.Buffer{}
66+
stderrBuf := bytes.Buffer{}
67+
68+
err = executil.Run(
69+
ctx,
70+
cmdCons,
71+
&executil.CommandConfig{
72+
Path: command,
73+
Args: arguments,
74+
Stdout: ioutil.NewTruncatedWriter(&stdoutBuf, MaxCmdOutputSize),
75+
Stderr: &stderrBuf,
76+
},
77+
)
78+
79+
if err == nil {
80+
return osutil.ExitCodeSuccess, stdoutBuf.Bytes(), nil
81+
}
6382

64-
return 1, nil, fmt.Errorf("command %q failed: %w: %s", command, err, out)
83+
code, ok := executil.ExitCodeFromError(err)
84+
if ok {
85+
// Mirror the old behavior and return a nil-error on non-zero code
86+
// status.
87+
return code, stderrBuf.Bytes(), nil
6588
}
6689

67-
return cmd.ProcessState.ExitCode(), out, nil
90+
return osutil.ExitCodeFailure,
91+
nil,
92+
fmt.Errorf("command %q failed: %w: %s", command, err, stdoutBuf.Bytes())
6893
}
6994

95+
// psArgs holds the default ps arguments to avoid per-call slice allocations.
96+
var psArgs = []string{"-A", "-o", "pid=", "-o", "comm="}
97+
7098
// PIDByCommand searches for process named command and returns its PID ignoring
7199
// the PIDs from except. If no processes found, the error returned. l must not
72100
// be nil.
@@ -76,31 +104,34 @@ func PIDByCommand(
76104
command string,
77105
except ...int,
78106
) (pid int, err error) {
107+
const psCmd = "ps"
108+
109+
l.DebugContext(ctx, "executing", "cmd", psCmd, "args", psArgs)
110+
79111
// Don't use -C flag here since it's a feature of linux's ps
80112
// implementation. Use POSIX-compatible flags instead.
81113
//
82114
// See https://github.com/AdguardTeam/AdGuardHome/issues/3457.
83-
cmd := exec.Command("ps", "-A", "-o", "pid=", "-o", "comm=")
84-
85-
var stdout io.ReadCloser
86-
if stdout, err = cmd.StdoutPipe(); err != nil {
87-
return 0, fmt.Errorf("getting the command's stdout pipe: %w", err)
88-
}
89-
90-
if err = cmd.Start(); err != nil {
91-
return 0, fmt.Errorf("start command executing: %w", err)
115+
stdoutBuf := bytes.Buffer{}
116+
err = executil.Run(
117+
ctx,
118+
executil.SystemCommandConstructor{},
119+
&executil.CommandConfig{
120+
Path: psCmd,
121+
Args: psArgs,
122+
Stdout: &stdoutBuf,
123+
},
124+
)
125+
if err != nil {
126+
return 0, fmt.Errorf("executing the command: %w", err)
92127
}
93128

94129
var instNum int
95-
pid, instNum, err = parsePSOutput(stdout, command, except)
130+
pid, instNum, err = parsePSOutput(&stdoutBuf, command, except)
96131
if err != nil {
97132
return 0, err
98133
}
99134

100-
if err = cmd.Wait(); err != nil {
101-
return 0, fmt.Errorf("executing the command: %w", err)
102-
}
103-
104135
switch instNum {
105136
case 0:
106137
// TODO(e.burkov): Use constant error.
@@ -111,10 +142,6 @@ func PIDByCommand(
111142
l.WarnContext(ctx, "instances found", "num", instNum, "command", command)
112143
}
113144

114-
if code := cmd.ProcessState.ExitCode(); code != 0 {
115-
return 0, fmt.Errorf("ps finished with code %d", code)
116-
}
117-
118145
return pid, nil
119146
}
120147

internal/arpdb/arpdb.go

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,23 @@ package arpdb
44
import (
55
"bufio"
66
"bytes"
7+
"context"
78
"fmt"
89
"log/slog"
910
"net"
1011
"net/netip"
1112
"slices"
1213
"sync"
1314

14-
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
1515
"github.com/AdguardTeam/golibs/errors"
1616
"github.com/AdguardTeam/golibs/logutil/slogutil"
1717
"github.com/AdguardTeam/golibs/netutil"
1818
"github.com/AdguardTeam/golibs/osutil"
19+
"github.com/AdguardTeam/golibs/osutil/executil"
1920
)
2021

2122
// Variables and functions to substitute in tests.
2223
var (
23-
// aghosRunCommand is the function to run shell commands.
24-
aghosRunCommand = aghos.RunCommand
25-
2624
// rootDirFS is the filesystem pointing to the root directory.
2725
rootDirFS = osutil.RootDirFS()
2826
)
@@ -40,7 +38,7 @@ type Interface interface {
4038

4139
// New returns the [Interface] properly initialized for the OS.
4240
func New(logger *slog.Logger) (arp Interface) {
43-
return newARPDB(logger)
41+
return newARPDB(logger, executil.SystemCommandConstructor{})
4442
}
4543

4644
// Empty is the [Interface] implementation that does nothing.
@@ -164,11 +162,12 @@ type parseNeighsFunc func(logger *slog.Logger, sc *bufio.Scanner, lenHint int) (
164162
// cmdARPDB is the implementation of the [Interface] that uses command line to
165163
// retrieve data.
166164
type cmdARPDB struct {
167-
logger *slog.Logger
168-
parse parseNeighsFunc
169-
ns *neighs
170-
cmd string
171-
args []string
165+
logger *slog.Logger
166+
cmdCons executil.CommandConstructor
167+
parse parseNeighsFunc
168+
ns *neighs
169+
cmd string
170+
args []string
172171
}
173172

174173
// type check
@@ -178,14 +177,26 @@ var _ Interface = (*cmdARPDB)(nil)
178177
func (arp *cmdARPDB) Refresh() (err error) {
179178
defer func() { err = errors.Annotate(err, "cmd arpdb: %w") }()
180179

181-
code, out, err := aghosRunCommand(arp.cmd, arp.args...)
180+
var stdout bytes.Buffer
181+
err = executil.Run(
182+
// TODO(s.chzhen): Pass context.
183+
context.TODO(),
184+
arp.cmdCons,
185+
&executil.CommandConfig{
186+
Path: arp.cmd,
187+
Args: arp.args,
188+
Stdout: &stdout,
189+
},
190+
)
182191
if err != nil {
192+
if code, ok := executil.ExitCodeFromError(err); ok {
193+
return fmt.Errorf("running command: unexpected exit code %d", code)
194+
}
195+
183196
return fmt.Errorf("running command: %w", err)
184-
} else if code != 0 {
185-
return fmt.Errorf("running command: unexpected exit code %d", code)
186197
}
187198

188-
sc := bufio.NewScanner(bytes.NewReader(out))
199+
sc := bufio.NewScanner(&stdout)
189200
ns := arp.parse(arp.logger, sc, arp.ns.len())
190201
if err = sc.Err(); err != nil {
191202
// TODO(e.burkov): This error seems unreachable. Investigate.

0 commit comments

Comments
 (0)