Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

record/history: Improve performance and ux while processing commands #66

Merged
merged 5 commits into from
Mar 11, 2024
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
72 changes: 40 additions & 32 deletions cmd/history.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/huh"
huhSpinner "github.com/charmbracelet/huh/spinner"
"github.com/creack/pty"
"github.com/getsavvyinc/savvy-cli/client"
"github.com/getsavvyinc/savvy-cli/cmd/component"
Expand All @@ -38,7 +39,7 @@ func init() {

func recordHistory(cmd *cobra.Command, _ []string) {
ctx := cmd.Context()
logger := loggerFromCtx(ctx)
logger := loggerFromCtx(ctx).With("command", "history")
cl, err := client.New()
if err != nil && errors.Is(err, client.ErrInvalidClient) {
display.Error(errors.New("You must be logged in to record a runbook. Please run `savvy login`"))
Expand All @@ -52,15 +53,27 @@ func recordHistory(cmd *cobra.Command, _ []string) {
}

selectedHistory := allowUserToSelectCommands(lines)
commands, err := expandHistory(logger, sh, selectedHistory)
if err != nil {
display.FatalErrWithSupportCTA(err)
}
if len(commands) == 0 {
display.Error(errors.New("No commands were recorded"))
if len(selectedHistory) == 0 {
display.Error(errors.New("No commands were selected"))
return
}

var commands []string
if err := huhSpinner.New().Title("Processing selected commands").Action(func() {
var err error

commands, err = expandHistory(logger, sh, selectedHistory)
if err != nil {
display.FatalErrWithSupportCTA(err)
}
if len(commands) == 0 {
display.Error(errors.New("No commands were recorded"))
return
}
}).Run(); err != nil {
logger.Debug("failed to run spinner", "error", err.Error())
}

gctx, cancel := context.WithCancel(ctx)
gm := component.NewGenerateRunbookModel(commands, cl)
p := tea.NewProgram(gm, tea.WithOutput(programOutput), tea.WithContext(gctx))
Expand Down Expand Up @@ -118,20 +131,22 @@ func allowUserToSelectCommands(history []string) (selectedHistory []string) {

func expandHistory(logger *slog.Logger, sh shell.Shell, rawCommands []string) ([]string, error) {
logger.Debug("expanding history", "commands", rawCommands)
socketPath := "/tmp/savvy-socket"
ss, err := server.NewUnixSocketServer(socketPath)

commandProcessedChan := make(chan bool, 1)

hook := func(cmd string) {
logger.Debug("command recorded", "command", cmd)
commandProcessedChan <- true
}
ss, err := server.NewUnixSocketServerWithDefaultPath(server.WithCommandRecordedHook(hook))
if err != nil {
return nil, err
}
go ss.ListenAndServe()
defer func() {
ss.Close()
}()
defer ss.Close()

ctx, cancelCtx := context.WithCancel(context.Background())
defer func() {
cancelCtx()
}()
defer cancelCtx()

c, err := sh.SpawnHistoryExpander(ctx)
if err != nil {
Expand All @@ -154,33 +169,26 @@ func expandHistory(logger *slog.Logger, sh shell.Shell, rawCommands []string) ([
io.Copy(io.Discard, ptmx)
}()

for _, cmd := range rawCommands {
for i, cmd := range rawCommands {
if _, err := fmt.Fprintln(ptmx, cmd); err != nil {
return nil, err
}
// Wait for the command to be processed by the server.
select {
case <-commandProcessedChan:
case <-time.After(5 * time.Second):
logger.Debug("timeout waiting for command to be processed", "command", cmd, "index", i)
}
}
ptmx.Write([]byte{4}) // EOT

for len(ss.Commands()) < len(rawCommands) {
// wait for all commands to be processed
logger.Debug("waiting for all commands to be processed", "processed", len(ss.Commands()), "total", len(rawCommands))
time.Sleep(1 * time.Second)
}
ptmx.Write([]byte{4}) // End Of Transmission (EOT) == Ctrl-D

logger.Debug("waiting for wg.Wait()")
wg.Wait()
logger.Debug("wg.Wait() finished")
logger.Debug("waitng for c.Wait()")
logger.Debug("canceling context for psuedy terminal and its associated command")
cancelCtx()
logger.Debug("waitng for c.Wait()")
c.Wait()
logger.Debug("c.Wait() finished")
return ss.Commands(), nil
}

// nullWriter implements the io.Writer interface and discards all data written to it.
type nullWriter struct{}

// Write discards the data written to the NullWriter.
func (nw nullWriter) Write(p []byte) (int, error) {
return len(p), nil
}
11 changes: 6 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@ module github.com/getsavvyinc/savvy-cli
go 1.21.6

require (
github.com/charmbracelet/bubbles v0.17.2-0.20240108170749-ec883029c8e6
github.com/charmbracelet/bubbles v0.18.0
github.com/charmbracelet/bubbletea v0.25.0
github.com/charmbracelet/huh v0.3.0
github.com/charmbracelet/huh/spinner v0.0.0-20240306161957-71f31c155b08
github.com/charmbracelet/lipgloss v0.9.1
github.com/creack/pty v1.1.21
github.com/getsavvyinc/upgrade-cli v0.3.0
github.com/muesli/cancelreader v0.2.2
github.com/muesli/termenv v0.15.2
github.com/spf13/cobra v1.8.0
github.com/stretchr/testify v1.8.4
golang.org/x/term v0.15.0
golang.org/x/term v0.16.0
)

require (
Expand All @@ -31,11 +32,11 @@ require (
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/rivo/uniseg v0.4.6 // indirect
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/sync v0.4.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
22 changes: 12 additions & 10 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA=
github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/charmbracelet/bubbles v0.17.2-0.20240108170749-ec883029c8e6 h1:6nVCV8pqGaeyxetur3gpX3AAaiyKgzjIoCPV3NXKZBE=
github.com/charmbracelet/bubbles v0.17.2-0.20240108170749-ec883029c8e6/go.mod h1:9HxZWlkCqz2PRwsCbYl7a3KXvGzFaDHpYbSYMJ+nE3o=
github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw=
github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
github.com/charmbracelet/huh v0.3.0 h1:CxPplWkgW2yUTDDG0Z4S5HH8SJOosWHd4LxCvi0XsKE=
github.com/charmbracelet/huh v0.3.0/go.mod h1:fujUdKX8tC45CCSaRQdw789O6uaCRwx8l2NDyKfC4jA=
github.com/charmbracelet/huh/spinner v0.0.0-20240306161957-71f31c155b08 h1:kO5eMMxyCJ6m7gdpGQ7OomrMdfsKVPgC4aB/focl/HE=
github.com/charmbracelet/huh/spinner v0.0.0-20240306161957-71f31c155b08/go.mod h1:nrBG0YEHaxdbqHXW1xvG1hPqkuac9Eg7RTMvogiXuz0=
github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I=
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
Expand Down Expand Up @@ -48,8 +50,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg=
github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y=
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
Expand All @@ -59,16 +61,16 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
31 changes: 24 additions & 7 deletions server/unix_socket.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ type UnixSocketServer struct {
socketPath string
listener net.Listener

mu sync.Mutex
commands []string
mu sync.Mutex
commands []string
commandRecordedHook func(string)

closed atomic.Bool
}
Expand All @@ -25,18 +26,26 @@ var ErrStartingRecordingSession = errors.New("failed to start recording session"

const defaultSocketPath = "/tmp/savvy-socket"

func NewUnixSocketServerWithDefaultPath() (*UnixSocketServer, error) {
return NewUnixSocketServer(defaultSocketPath)
type Option func(*UnixSocketServer)

func WithCommandRecordedHook(hook func(string)) Option {
return func(s *UnixSocketServer) {
s.commandRecordedHook = hook
}
}

func NewUnixSocketServerWithDefaultPath(opts ...Option) (*UnixSocketServer, error) {
return NewUnixSocketServer(defaultSocketPath, opts...)
}

func NewUnixSocketServer(socketPath string) (*UnixSocketServer, error) {
func NewUnixSocketServer(socketPath string, opts ...Option) (*UnixSocketServer, error) {
if fileInfo, _ := os.Stat(socketPath); fileInfo != nil {
return nil, fmt.Errorf("%w: concurrent recording sessions are not supported yet", ErrStartingRecordingSession)
}
return newUnixSocketServer(socketPath)
return newUnixSocketServer(socketPath, opts...)
}

func newUnixSocketServer(socketPath string) (*UnixSocketServer, error) {
func newUnixSocketServer(socketPath string, opts ...Option) (*UnixSocketServer, error) {
listener, err := net.Listen("unix", socketPath)
if err != nil {
return nil, fmt.Errorf("failed to create listener: %w", err)
Expand All @@ -46,6 +55,11 @@ func newUnixSocketServer(socketPath string) (*UnixSocketServer, error) {
socketPath: socketPath,
listener: listener,
}

for _, opt := range opts {
opt(srv)
}

return srv, nil
}

Expand Down Expand Up @@ -91,6 +105,9 @@ func (s *UnixSocketServer) handleConnection(c net.Conn) {
}
command := string(bs)
s.appendCommand(command)
if s.commandRecordedHook != nil {
s.commandRecordedHook(command)
}
}

func (s *UnixSocketServer) SocketPath() string {
Expand Down
5 changes: 3 additions & 2 deletions shell/zsh.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,9 @@ function __savvy_history_pre_exec__ {
local cmd=${3}

if [[ -n "${cmd}" ]]; then
SAVVY_SOCKET_PATH=${SAVVY_INPUT_FILE} savvy send "$cmd"
sleep 0.1
# Send the command to the unix socket server
# Running it as a b/g process is intentional here
SAVVY_SOCKET_PATH=${SAVVY_INPUT_FILE} savvy send "$cmd" &
fi
# This is how we prevent the command from being executed
exec zsh
Expand Down