Skip to content

Commit 8d5bf91

Browse files
committed
Added process.Background() and process.Forwarded()
This PR adds higher-level wrappers for calling subprocesses
1 parent 757d5ef commit 8d5bf91

File tree

11 files changed

+237
-22
lines changed

11 files changed

+237
-22
lines changed

bundle/config/artifact.go

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import (
44
"bytes"
55
"context"
66
"fmt"
7-
"os/exec"
87
"path"
98
"strings"
109

1110
"github.com/databricks/cli/bundle/config/paths"
11+
"github.com/databricks/cli/libs/process"
1212
"github.com/databricks/databricks-sdk-go/service/compute"
1313
)
1414

@@ -56,13 +56,11 @@ func (a *Artifact) Build(ctx context.Context) ([]byte, error) {
5656
commands := strings.Split(a.BuildCommand, " && ")
5757
for _, command := range commands {
5858
buildParts := strings.Split(command, " ")
59-
cmd := exec.CommandContext(ctx, buildParts[0], buildParts[1:]...)
60-
cmd.Dir = a.Path
61-
res, err := cmd.CombinedOutput()
59+
res, err := process.Background(ctx, buildParts, process.WithDir(a.Path))
6260
if err != nil {
63-
return res, err
61+
return nil, err
6462
}
65-
out = append(out, res)
63+
out = append(out, []byte(res))
6664
}
6765
return bytes.Join(out, []byte{}), nil
6866
}

bundle/scripts/scripts.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ func executeHook(ctx context.Context, b *bundle.Bundle, hook config.ScriptHook)
6161
return nil, nil, err
6262
}
6363

64+
// TODO: switch to process.Background(...)
6465
cmd := exec.CommandContext(ctx, interpreter, "-c", string(command))
6566
cmd.Dir = b.Config.Path
6667

libs/git/clone.go

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
package git
22

33
import (
4-
"bytes"
54
"context"
65
"errors"
76
"fmt"
87
"os/exec"
98
"regexp"
109
"strings"
10+
11+
"github.com/databricks/cli/libs/process"
1112
)
1213

1314
// source: https://stackoverflow.com/questions/59081778/rules-for-special-characters-in-github-repository-name
@@ -42,24 +43,14 @@ func (opts cloneOptions) args() []string {
4243
}
4344

4445
func (opts cloneOptions) clone(ctx context.Context) error {
45-
cmd := exec.CommandContext(ctx, "git", opts.args()...)
46-
var cmdErr bytes.Buffer
47-
cmd.Stderr = &cmdErr
48-
49-
// start git clone
50-
err := cmd.Start()
46+
// start and wait for git clone to complete
47+
_, err := process.Background(ctx, append([]string{"git"}, opts.args()...))
5148
if errors.Is(err, exec.ErrNotFound) {
5249
return fmt.Errorf("please install git CLI to clone a repository: %w", err)
5350
}
5451
if err != nil {
5552
return fmt.Errorf("git clone failed: %w", err)
5653
}
57-
58-
// wait for git clone to complete
59-
err = cmd.Wait()
60-
if err != nil {
61-
return fmt.Errorf("git clone failed: %w. %s", err, cmdErr.String())
62-
}
6354
return nil
6455
}
6556

libs/process/background.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package process
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"os"
8+
"os/exec"
9+
"strings"
10+
11+
"github.com/databricks/cli/libs/log"
12+
)
13+
14+
func Background(ctx context.Context, args []string, opts ...execOption) (string, error) {
15+
commandStr := strings.Join(args, " ")
16+
log.Debugf(ctx, "running: %s", commandStr)
17+
cmd := exec.CommandContext(ctx, args[0], args[1:]...)
18+
stdout := &bytes.Buffer{}
19+
cmd.Stdin = os.Stdin
20+
cmd.Stdout = stdout
21+
cmd.Stderr = stdout
22+
for _, o := range opts {
23+
err := o(cmd)
24+
if err != nil {
25+
return "", err
26+
}
27+
}
28+
if err := cmd.Run(); err != nil {
29+
return "", fmt.Errorf("%s: %s %w", commandStr, stdout.String(), err)
30+
}
31+
return strings.TrimSpace(stdout.String()), nil
32+
}

libs/process/background_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package process
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os/exec"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestBackground(t *testing.T) {
13+
ctx := context.Background()
14+
res, err := Background(ctx, []string{"echo", "1"}, WithDir("/"))
15+
assert.NoError(t, err)
16+
assert.Equal(t, "1", res)
17+
}
18+
19+
func TestBackgroundFails(t *testing.T) {
20+
ctx := context.Background()
21+
_, err := Background(ctx, []string{"ls", "/dev/null/x"})
22+
assert.NotNil(t, err)
23+
}
24+
25+
func TestBackgroundFailsOnOption(t *testing.T) {
26+
ctx := context.Background()
27+
_, err := Background(ctx, []string{"ls", "/dev/null/x"}, func(c *exec.Cmd) error {
28+
return fmt.Errorf("nope")
29+
})
30+
assert.EqualError(t, err, "nope")
31+
}

libs/process/forwarded.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package process
2+
3+
import (
4+
"context"
5+
"io"
6+
"os/exec"
7+
"strings"
8+
9+
"github.com/databricks/cli/libs/log"
10+
)
11+
12+
func Forwarded(ctx context.Context, args []string, src io.Reader, dst io.Writer, opts ...execOption) error {
13+
commandStr := strings.Join(args, " ")
14+
log.Debugf(ctx, "starting: %s", commandStr)
15+
cmd := exec.CommandContext(ctx, args[0], args[1:]...)
16+
17+
// make sure to sync on writing to stdout
18+
reader, writer := io.Pipe()
19+
go io.CopyBuffer(dst, reader, make([]byte, 128))
20+
defer reader.Close()
21+
defer writer.Close()
22+
cmd.Stdout = writer
23+
cmd.Stderr = writer
24+
25+
// apply common options
26+
for _, o := range opts {
27+
err := o(cmd)
28+
if err != nil {
29+
return err
30+
}
31+
}
32+
33+
// pipe standard input to the child process, so that we can allow terminal UX
34+
// see the PoC at https://github.com/databricks/cli/pull/637
35+
stdin, err := cmd.StdinPipe()
36+
if err != nil {
37+
return err
38+
}
39+
go io.CopyBuffer(stdin, src, make([]byte, 128))
40+
defer stdin.Close()
41+
42+
err = cmd.Start()
43+
if err != nil {
44+
return err
45+
}
46+
47+
return cmd.Wait()
48+
}

libs/process/forwarded_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package process
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"os/exec"
7+
"strings"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func TestForwarded(t *testing.T) {
14+
ctx := context.Background()
15+
buf := bytes.NewBufferString("")
16+
err := Forwarded(ctx, []string{
17+
"python3", "-c", "print(input('input: '))",
18+
}, strings.NewReader("abc\n"), buf)
19+
assert.NoError(t, err)
20+
21+
assert.Equal(t, "input: abc\n", buf.String())
22+
}
23+
24+
func TestForwardedFails(t *testing.T) {
25+
ctx := context.Background()
26+
buf := bytes.NewBufferString("")
27+
err := Forwarded(ctx, []string{
28+
"_non_existent_",
29+
}, strings.NewReader("abc\n"), buf)
30+
assert.NotNil(t, err)
31+
}
32+
33+
func TestForwardedFailsOnStdinPipe(t *testing.T) {
34+
ctx := context.Background()
35+
buf := bytes.NewBufferString("")
36+
err := Forwarded(ctx, []string{
37+
"_non_existent_",
38+
}, strings.NewReader("abc\n"), buf, func(c *exec.Cmd) error {
39+
c.Stdin = strings.NewReader("x")
40+
return nil
41+
})
42+
assert.NotNil(t, err)
43+
}

libs/process/opts.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package process
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"os"
7+
"os/exec"
8+
)
9+
10+
type execOption func(*exec.Cmd) error
11+
12+
func WithEnv(key, value string) execOption {
13+
return func(c *exec.Cmd) error {
14+
if c.Env == nil {
15+
c.Env = os.Environ()
16+
}
17+
v := fmt.Sprintf("%s=%s", key, value)
18+
c.Env = append(c.Env, v)
19+
return nil
20+
}
21+
}
22+
23+
func WithEnvs(envs map[string]string) execOption {
24+
return func(c *exec.Cmd) error {
25+
for k, v := range envs {
26+
err := WithEnv(k, v)(c)
27+
if err != nil {
28+
return err
29+
}
30+
}
31+
return nil
32+
}
33+
}
34+
35+
func WithDir(dir string) execOption {
36+
return func(c *exec.Cmd) error {
37+
c.Dir = dir
38+
return nil
39+
}
40+
}
41+
42+
func WithStdoutPipe(dst *io.ReadCloser) execOption {
43+
return func(c *exec.Cmd) error {
44+
outPipe, err := c.StdoutPipe()
45+
if err != nil {
46+
return err
47+
}
48+
*dst = outPipe
49+
return nil
50+
}
51+
}

libs/process/opts_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package process
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestWithEnvs(t *testing.T) {
11+
ctx := context.Background()
12+
res, err := Background(ctx, []string{"/bin/sh", "-c", "echo $FOO $BAR"}, WithEnvs(map[string]string{
13+
"FOO": "foo",
14+
"BAR": "delirium",
15+
}))
16+
assert.NoError(t, err)
17+
assert.Equal(t, "foo delirium", res)
18+
}

python/runner.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"os/exec"
99
"runtime"
1010
"strings"
11+
12+
"github.com/databricks/cli/libs/process"
1113
)
1214

1315
func PyInline(ctx context.Context, inlinePy string) (string, error) {
@@ -88,8 +90,8 @@ func DetectExecutable(ctx context.Context) (string, error) {
8890

8991
func execAndPassErr(ctx context.Context, name string, args ...string) ([]byte, error) {
9092
// TODO: move out to a separate package, once we have Maven integration
91-
out, err := exec.CommandContext(ctx, name, args...).Output()
92-
return out, nicerErr(err)
93+
out, err := process.Background(ctx, append([]string{name}, args...))
94+
return []byte(out), nicerErr(err)
9395
}
9496

9597
func getFirstMatch(out string) string {

0 commit comments

Comments
 (0)