Skip to content
Open
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
17 changes: 15 additions & 2 deletions cmd/apps/run_local.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,15 @@ func setupProxy(ctx context.Context, cmd *cobra.Command, config *runlocal.Config
}

proxyAddr := fmt.Sprintf("localhost:%d", port)
// Bind synchronously so a taken port fails the command instead of only printing an error from the goroutine.
ln, err := proxy.Listen(proxyAddr)
if err != nil {
return fmt.Errorf("failed to start app proxy: %w", err)
}

cmdio.LogString(ctx, "To access your app go to http://"+proxyAddr)
go func() {
cmdio.LogString(ctx, "To access your app go to http://"+proxyAddr)
err := proxy.ListenAndServe(proxyAddr)
err := proxy.Serve(ln)
if err != nil {
cmd.PrintErrln(err)
}
Expand All @@ -142,6 +148,12 @@ func setupProxy(ctx context.Context, cmd *cobra.Command, config *runlocal.Config
return nil
}

func killAppProcess(appCmd *exec.Cmd) {
_ = appCmd.Process.Kill()
// Reap the process so it doesn't linger as a zombie until the CLI exits.
_ = appCmd.Wait()
}

// SIGTERM (not supported on Windows) and SIGINT (Ctrl+C, supported cross-platform)
// are caught to enable graceful shutdown of the app process.
func handleGracefulShutdown(appCmd *exec.Cmd) error {
Expand Down Expand Up @@ -226,6 +238,7 @@ func newRunLocal() *cobra.Command {

err = setupProxy(ctx, cmd, config, w, port, debug)
if err != nil {
killAppProcess(appCmd)
return err
}

Expand Down
51 changes: 51 additions & 0 deletions cmd/apps/run_local_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package apps

import (
"net"
"os"
"os/exec"
"testing"
"time"

"github.com/databricks/cli/libs/apps/runlocal"
"github.com/databricks/databricks-sdk-go/experimental/mocks"
"github.com/databricks/databricks-sdk-go/service/iam"
"github.com/spf13/cobra"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)

func TestSetupProxyPortInUse(t *testing.T) {
ln, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err)
defer ln.Close()
port := ln.Addr().(*net.TCPAddr).Port

m := mocks.NewMockWorkspaceClient(t)
m.GetMockCurrentUserAPI().EXPECT().Me(mock.Anything, mock.Anything).Return(&iam.User{UserName: "test-user"}, nil)

config := runlocal.NewConfig("https://workspace.databricks.test", "123", t.TempDir(), runlocal.DEFAULT_HOST, runlocal.DEFAULT_PORT)
err = setupProxy(t.Context(), &cobra.Command{}, config, m.WorkspaceClient, port, false)
require.ErrorContains(t, err, "failed to start app proxy")
}

// TestAppHelperProcess is not a real test: TestKillAppProcess re-invokes the
// test binary with -test.run targeting it to get a long-running child process.
func TestAppHelperProcess(t *testing.T) {
if os.Getenv("APPS_TEST_HELPER_PROCESS") != "1" {
t.Skip("helper process for TestKillAppProcess")
}
time.Sleep(time.Minute)
}

func TestKillAppProcess(t *testing.T) {
appCmd := exec.Command(os.Args[0], "-test.run=^TestAppHelperProcess$")
appCmd.Env = append(os.Environ(), "APPS_TEST_HELPER_PROCESS=1")
require.NoError(t, appCmd.Start())

killAppProcess(appCmd)

// A non-nil ProcessState proves the process was reaped; a non-success exit proves it was killed.
require.NotNil(t, appCmd.ProcessState)
require.False(t, appCmd.ProcessState.Success())
}
16 changes: 4 additions & 12 deletions libs/appproxy/appproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,24 +33,16 @@ func New(ctx context.Context, targetURL string) (*Proxy, error) {
return &proxy, nil
}

func (p *Proxy) listen(addr string) (net.Listener, error) {
// Listen binds the proxy to the given address (host:port, e.g. localhost:8080).
func (p *Proxy) Listen(addr string) (net.Listener, error) {
return net.Listen("tcp", addr)
}

func (p *Proxy) serve(ln net.Listener) error {
// Serve accepts connections on the given listener and forwards all requests to the targetURL.
func (p *Proxy) Serve(ln net.Listener) error {
return p.server.Serve(ln)
}

// ListenAndServe starts the proxy server on the given address (host:port, e.g. localhost:8080)
// The proxy will forward all requests to the targetURL
func (p *Proxy) ListenAndServe(addr string) error {
ln, err := p.listen(addr)
if err != nil {
return err
}
return p.serve(ln)
}

func (p *Proxy) Stop() error {
return p.server.Shutdown(p.ctx)
}
Expand Down
4 changes: 2 additions & 2 deletions libs/appproxy/appproxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ func startProxy(t *testing.T, serverAddr string) (*Proxy, string) {
proxy, err := New(t.Context(), "http://"+serverAddr)
require.NoError(t, err)

ln, err := proxy.listen(fmt.Sprintf("localhost:%d", PROXY_PORT))
ln, err := proxy.Listen(fmt.Sprintf("localhost:%d", PROXY_PORT))
require.NoError(t, err)

go func() {
_ = proxy.serve(ln)
_ = proxy.Serve(ln)
}()

return proxy, ln.Addr().String()
Expand Down
Loading