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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/20251209165547.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:sparkles: [subprocess] add helpers for starting processes
22 changes: 21 additions & 1 deletion utils/subprocess/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,21 @@ func ExecuteWithEnvironment(ctx context.Context, loggers logs.Loggers, additiona
return ExecuteWithEnvironmentWithIO(ctx, loggers, nil, additionalEnvVars, messageOnStart, messageOnSuccess, messageOnFailure, cmd, args...)
}

// StartWithEnvironment starts a command (i.e. spawns a subprocess). It allows to specify the environment the subprocess should use. Each entry is of the form "key=value".
func StartWithEnvironment(ctx context.Context, loggers logs.Loggers, additionalEnvVars []string, messageOnStart string, messageOnSuccess, messageOnFailure string, cmd string, args ...string) (*Subprocess, error) {
return StartWithEnvironmentWithIO(ctx, loggers, nil, additionalEnvVars, messageOnStart, messageOnSuccess, messageOnFailure, cmd, args...)
}

// Execute executes a command (i.e. spawns a subprocess).
func Execute(ctx context.Context, loggers logs.Loggers, messageOnStart string, messageOnSuccess, messageOnFailure string, cmd string, args ...string) error {
return ExecuteWithEnvironment(ctx, loggers, nil, messageOnStart, messageOnSuccess, messageOnFailure, cmd, args...)
}

// Start starts a command (i.e. spawns a subprocess).
func Start(ctx context.Context, loggers logs.Loggers, messageOnStart string, messageOnSuccess, messageOnFailure string, cmd string, args ...string) (*Subprocess, error) {
return StartWithEnvironment(ctx, loggers, nil, messageOnStart, messageOnSuccess, messageOnFailure, cmd, args...)
}

// ExecuteAs executes a command (i.e. spawns a subprocess) as a different user.
func ExecuteAs(ctx context.Context, loggers logs.Loggers, messageOnStart string, messageOnSuccess, messageOnFailure string, as *commandUtils.CommandAsDifferentUser, cmd string, args ...string) error {
return ExecuteAsWithEnvironment(ctx, loggers, nil, messageOnStart, messageOnSuccess, messageOnFailure, as, cmd, args...)
Expand All @@ -86,7 +96,7 @@ func ExecuteWithSudo(ctx context.Context, loggers logs.Loggers, messageOnStart s
return ExecuteAs(ctx, loggers, messageOnStart, messageOnSuccess, messageOnFailure, commandUtils.Sudo(), cmd, args...)
}

// ExecuteWithEnvironment executes a command (i.e. spawns a subprocess) with overridden stdin/stdout/stderr. It allows to specify the environment the subprocess should use. Each entry is of the form "key=value".
// ExecuteWithEnvironmentWithIO executes a command (i.e. spawns a subprocess) with overridden stdin/stdout/stderr. It allows to specify the environment the subprocess should use. Each entry is of the form "key=value".
func ExecuteWithEnvironmentWithIO(ctx context.Context, loggers logs.Loggers, io ICommandIO, additionalEnvVars []string, messageOnStart string, messageOnSuccess, messageOnFailure string, cmd string, args ...string) (err error) {
p, err := NewWithEnvironmentWithIO(ctx, loggers, io, additionalEnvVars, messageOnStart, messageOnSuccess, messageOnFailure, cmd, args...)
if err != nil {
Expand All @@ -95,6 +105,16 @@ func ExecuteWithEnvironmentWithIO(ctx context.Context, loggers logs.Loggers, io
return p.Execute()
}

// StartWithEnvironmentWithIO starts a command (i.e. spawns a subprocess) with overridden stdin/stdout/stderr. It allows to specify the environment the subprocess should use. Each entry is of the form "key=value".
func StartWithEnvironmentWithIO(ctx context.Context, loggers logs.Loggers, io ICommandIO, additionalEnvVars []string, messageOnStart string, messageOnSuccess, messageOnFailure string, cmd string, args ...string) (p *Subprocess, err error) {
p, err = NewWithEnvironmentWithIO(ctx, loggers, io, additionalEnvVars, messageOnStart, messageOnSuccess, messageOnFailure, cmd, args...)
if err != nil {
return
}
err = p.Start()
return
}

// ExecuteWithIO executes a command (i.e. spawns a subprocess) with overridden stdin/stdout/stderr.
func ExecuteWithIO(ctx context.Context, loggers logs.Loggers, io ICommandIO, messageOnStart string, messageOnSuccess, messageOnFailure string, cmd string, args ...string) error {
return ExecuteWithEnvironmentWithIO(ctx, loggers, io, nil, messageOnStart, messageOnSuccess, messageOnFailure, cmd, args...)
Expand Down
95 changes: 95 additions & 0 deletions utils/subprocess/executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func (t *testIO) Register(context.Context) (io.Reader, io.Writer, io.Writer) {
}

type execFunc func(ctx context.Context, l logs.Loggers, cmd string, args ...string) error
type startFunc func(ctx context.Context, l logs.Loggers, cmd string, args ...string) (*Subprocess, error)

func newDefaultExecutor(t *testing.T) execFunc {
t.Helper()
Expand All @@ -68,6 +69,26 @@ func newCustomIOExecutor(t *testing.T, customIO *testIO) execFunc {
}
}

func newDefaultStarter(t *testing.T) startFunc {
t.Helper()
return func(ctx context.Context, l logs.Loggers, cmd string, args ...string) (*Subprocess, error) {
return Start(ctx, l, "", "", "", cmd, args...)
}
}

func newCustomIOStarter(t *testing.T, customIO *testIO) startFunc {
t.Helper()
return func(ctx context.Context, l logs.Loggers, cmd string, args ...string) (p *Subprocess, err error) {
p = &Subprocess{}
err = p.SetupWithEnvironmentWithCustomIO(ctx, l, customIO, nil, "", "", "", cmd, args...)
if err != nil {
return
}
err = p.Start()
return
}
}

func TestExecuteEmptyLines(t *testing.T) {
t.Skip("would need to be reinstated when fixed")
defer goleak.VerifyNone(t)
Expand Down Expand Up @@ -375,6 +396,80 @@ func TestExecute(t *testing.T) {
}
}

func TestStart(t *testing.T) {

currentDir, err := os.Getwd()
require.NoError(t, err)

tests := []struct {
name string
cmdWindows string
argWindows []string
cmdOther string
argOther []string
expectIO bool
}{
{
name: "ShortProcess",
cmdWindows: "cmd",
argWindows: []string{"/c", "dir", currentDir},
cmdOther: "ls",
argOther: []string{"-l", currentDir},
expectIO: true,
},
{
name: "LongProcess",
cmdWindows: "cmd",
argWindows: []string{"/c", fmt.Sprintf("ping -n 2 -w %v localhost > nul", time.Second.Milliseconds())}, // See https://stackoverflow.com/a/79268314/45375
cmdOther: "sleep",
argOther: []string{"1"},
expectIO: false,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
defer goleak.VerifyNone(t)

customIO := newTestIO()
executors := []struct {
name string
start startFunc
io *testIO
}{
{"normal", newDefaultStarter(t), nil},
{"with IO", newCustomIOStarter(t, customIO), customIO},
}

for _, executor := range executors {
t.Run(executor.name, func(t *testing.T) {
var loggers logs.Loggers = &logs.GenericLoggers{}
err := loggers.Check()
assert.Error(t, err)

_, err = executor.start(context.Background(), loggers, "ls")
assert.Error(t, err)

loggers, err = logs.NewLogrLogger(logstest.NewTestLogger(t), "test")
require.NoError(t, err)

var p *Subprocess
if platform.IsWindows() {
p, err = executor.start(context.Background(), loggers, test.cmdWindows, test.argWindows...)
} else {
p, err = executor.start(context.Background(), loggers, test.cmdOther, test.argOther...)
}
require.NoError(t, err)
require.NotNil(t, p)
defer func() { _ = p.Stop() }()
require.NoError(t, p.Wait(context.Background()))
p.Cancel()
})
}
})
}
}

func TestExecuteWithCustomIO_Stderr(t *testing.T) {
if platform.IsWindows() {
t.Skip("Uses bash and redirection so can't run on Windows")
Expand Down
Loading