Skip to content

Commit

Permalink
fix: interrupt plugin commands
Browse files Browse the repository at this point in the history
Due to the plugin architecture, the user wasn't able to interrupt a
plugin command via Ctrl+C. This is annoying if the plugin execution is
long.

By running the plugin execution in a goroutine and by listening to the
command context at the same time, we can fix that.
  • Loading branch information
tbruyelle committed Dec 2, 2022
1 parent 8ccb10d commit e64a36a
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 17 deletions.
34 changes: 17 additions & 17 deletions ignite/cmd/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/spf13/cobra"

pluginsconfig "github.com/ignite/cli/ignite/config/plugins"
"github.com/ignite/cli/ignite/pkg/clictx"
"github.com/ignite/cli/ignite/pkg/cliui"
"github.com/ignite/cli/ignite/pkg/xgit"
"github.com/ignite/cli/ignite/services/plugin"
Expand Down Expand Up @@ -258,23 +259,22 @@ func linkPluginCmd(rootCmd *cobra.Command, p *plugin.Plugin, pluginCmd plugin.Co
if len(pluginCmd.Commands) == 0 {
// pluginCmd has no sub commands, so it's runnable
newCmd.RunE = func(cmd *cobra.Command, args []string) error {
execCmd := plugin.ExecutedCommand{
Use: cmd.Use,
Path: cmd.CommandPath(),
Args: args,
With: p.With,
}
execCmd.SetFlags(cmd.Flags())
// Call the plugin Execute
err := p.Interface.Execute(execCmd)
// NOTE(tb): This pause gives enough time for go-plugin to sync the
// output from stdout/stderr of the plugin. Without that pause, this
// output can be discarded and not printed in the user console.
time.Sleep(100 * time.Millisecond)
if err != nil {
return fmt.Errorf("plugin %q Execute() error : %w", p.Path, err)
}
return nil
return clictx.Do(cmd.Context(), func() error {
execCmd := plugin.ExecutedCommand{
Use: cmd.Use,
Path: cmd.CommandPath(),
Args: args,
With: p.With,
}
execCmd.SetFlags(cmd.Flags())
// Call the plugin Execute
err := p.Interface.Execute(execCmd)
// NOTE(tb): This pause gives enough time for go-plugin to sync the
// output from stdout/stderr of the plugin. Without that pause, this
// output can be discarded and not printed in the user console.
time.Sleep(100 * time.Millisecond)
return err
})
}
} else {
for _, pluginCmd := range pluginCmd.Commands {
Expand Down
13 changes: 13 additions & 0 deletions ignite/pkg/clictx/clictx.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,16 @@ func From(ctx context.Context) context.Context {
}()
return ctxend
}

// Do runs fn and waits for its result unless ctx is canceled.
// Returns fn result or canceled context error.
func Do(ctx context.Context, fn func() error) error {
errc := make(chan error)
go func() { errc <- fn() }()
select {
case err := <-errc:
return err
case <-ctx.Done():
return ctx.Err()
}
}
57 changes: 57 additions & 0 deletions ignite/pkg/clictx/clictx_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package clictx_test

import (
"context"
"errors"
"testing"
"time"

"github.com/stretchr/testify/require"

"github.com/ignite/cli/ignite/pkg/clictx"
)

func TestDo(t *testing.T) {
ctxCanceled, cancel := context.WithCancel(context.Background())
cancel()
tests := []struct {
name string
ctx context.Context
f func() error
expectedErr string
}{
{
name: "f returns nil",
ctx: context.Background(),
f: func() error { return nil },
},
{
name: "f returns an error",
ctx: context.Background(),
f: func() error { return errors.New("oups") },
expectedErr: "oups",
},
{
name: "ctx is canceled",
ctx: ctxCanceled,
f: func() error {
time.Sleep(time.Second)
return nil
},
expectedErr: context.Canceled.Error(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require := require.New(t)

err := clictx.Do(tt.ctx, tt.f)

if tt.expectedErr != "" {
require.EqualError(err, tt.expectedErr)
return
}
require.NoError(err)
})
}
}

0 comments on commit e64a36a

Please sign in to comment.