-
Notifications
You must be signed in to change notification settings - Fork 78
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add wish.Command and wish.Cmd (#229)
* feat: add wish.Command and wish.Cmd The wish-exec example with vim worked because neovim was not using STDERR. Bubbletea doesn't have a concept of stdout and stderr, just output, so `tea.ExecProcess` sets the `exec.Cmd` stderr to `os.Stderr`. This would fail for bash, for instance. This also introduces a `wish.Cmd` type and a `wish.Command` function to properly set up a `wish.Cmd` based on `ssh.Session` (and optionally a Pty), which can then be used with `tea.Exec`. Finally, it adds to the wish-exec example, including the `s` key to run a shell (bash). closes #228 Signed-off-by: Carlos Alexandro Becker <[email protected]> * feat: allow to set dir and env * refactory: move things around a bit Signed-off-by: Carlos Alexandro Becker <[email protected]> * fix: loop * test: add some tests Signed-off-by: Carlos Alexandro Becker <[email protected]> * test: more tests Signed-off-by: Carlos Alexandro Becker <[email protected]> * test: more tests Signed-off-by: Carlos Alexandro Becker <[email protected]> * fix: improve tests Signed-off-by: Carlos Alexandro Becker <[email protected]> * test: windows Signed-off-by: Carlos Alexandro Becker <[email protected]> * fix: review Signed-off-by: Carlos Alexandro Becker <[email protected]> * fix: improve cmd * fix: create a new pty for exec.Cmd (#230) * fix: create a new pty for exec.Cmd Signed-off-by: Carlos Alexandro Becker <[email protected]> * fix: improvements * fix: test * fix: windows Signed-off-by: Carlos Alexandro Becker <[email protected]> * fix: unneeded err Signed-off-by: Carlos Alexandro Becker <[email protected]> * fix Signed-off-by: Carlos Alexandro Becker <[email protected]> * fix: resize && race Signed-off-by: Carlos Alexandro Becker <[email protected]> * fix: windows/linux Signed-off-by: Carlos Alexandro Becker <[email protected]> * test: ignore on windows for now Signed-off-by: Carlos Alexandro Becker <[email protected]> * fix: sync Signed-off-by: Carlos Alexandro Becker <[email protected]> * test: hammer Signed-off-by: Carlos Alexandro Becker <[email protected]> * chore: import --------- Signed-off-by: Carlos Alexandro Becker <[email protected]> * fix: update ssh Signed-off-by: Carlos Alexandro Becker <[email protected]> --------- Signed-off-by: Carlos Alexandro Becker <[email protected]>
- Loading branch information
Showing
7 changed files
with
279 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
package wish | ||
|
||
import ( | ||
"context" | ||
"io" | ||
"os/exec" | ||
|
||
tea "github.com/charmbracelet/bubbletea" | ||
"github.com/charmbracelet/ssh" | ||
) | ||
|
||
// CommandContext is like Command but includes a context. | ||
// | ||
// If the current session does not have a PTY, it sets them to the session | ||
// itself. | ||
func CommandContext(ctx context.Context, s ssh.Session, name string, args ...string) *Cmd { | ||
cmd := exec.CommandContext(ctx, name, args...) | ||
return &Cmd{s, cmd} | ||
} | ||
|
||
// Command sets stdin, stdout, and stderr to the current session's PTY slave. | ||
// | ||
// If the current session does not have a PTY, it sets them to the session | ||
// itself. | ||
// | ||
// This will call CommandContext using the session's Context. | ||
func Command(s ssh.Session, name string, args ...string) *Cmd { | ||
return CommandContext(s.Context(), s, name, args...) | ||
} | ||
|
||
// Cmd wraps a *exec.Cmd and a ssh.Pty so a command can be properly run. | ||
type Cmd struct { | ||
sess ssh.Session | ||
cmd *exec.Cmd | ||
} | ||
|
||
// SetDir set the underlying exec.Cmd env. | ||
func (c *Cmd) SetEnv(env []string) { | ||
c.cmd.Env = env | ||
} | ||
|
||
// Environ returns the underlying exec.Cmd environment. | ||
func (c *Cmd) Environ() []string { | ||
return c.cmd.Environ() | ||
} | ||
|
||
// SetDir set the underlying exec.Cmd dir. | ||
func (c *Cmd) SetDir(dir string) { | ||
c.cmd.Dir = dir | ||
} | ||
|
||
// Run runs the program and waits for it to finish. | ||
func (c *Cmd) Run() error { | ||
ppty, winCh, ok := c.sess.Pty() | ||
if !ok { | ||
c.cmd.Stdin, c.cmd.Stdout, c.cmd.Stderr = c.sess, c.sess, c.sess | ||
return c.cmd.Run() | ||
} | ||
return c.doRun(ppty, winCh) | ||
} | ||
|
||
var _ tea.ExecCommand = &Cmd{} | ||
|
||
// SetStderr conforms with tea.ExecCommand. | ||
func (*Cmd) SetStderr(io.Writer) {} | ||
|
||
// SetStdin conforms with tea.ExecCommand. | ||
func (*Cmd) SetStdin(io.Reader) {} | ||
|
||
// SetStdout conforms with tea.ExecCommand. | ||
func (*Cmd) SetStdout(io.Writer) {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
package wish | ||
|
||
import ( | ||
"bytes" | ||
"runtime" | ||
"strings" | ||
"testing" | ||
"time" | ||
|
||
"github.com/charmbracelet/ssh" | ||
"github.com/charmbracelet/wish/testsession" | ||
) | ||
|
||
func TestCommandNoPty(t *testing.T) { | ||
tmp := t.TempDir() | ||
sess := testsession.New(t, &ssh.Server{ | ||
Handler: func(s ssh.Session) { | ||
runEcho(s, "hello") | ||
runEnv(s, []string{"HELLO=world"}) | ||
runPwd(s, tmp) | ||
}, | ||
}, nil) | ||
var stdout bytes.Buffer | ||
var stderr bytes.Buffer | ||
sess.Stdout = &stdout | ||
sess.Stderr = &stderr | ||
if err := sess.Run(""); err != nil { | ||
t.Errorf("expected no error, got %v: %s", err, stderr.String()) | ||
} | ||
out := stdout.String() | ||
expectContains(t, out, "hello") | ||
expectContains(t, out, "HELLO=world") | ||
expectContains(t, out, tmp) | ||
} | ||
|
||
func TestCommandPty(t *testing.T) { | ||
if runtime.GOOS == "windows" { | ||
t.Skip() | ||
} | ||
tmp := t.TempDir() | ||
srv := &ssh.Server{ | ||
Handler: func(s ssh.Session) { | ||
runEcho(s, "hello") | ||
runEnv(s, []string{"HELLO=world"}) | ||
runPwd(s, tmp) | ||
// for some reason sometimes on macos github action runners, | ||
// it cuts parts of the output. | ||
time.Sleep(100 * time.Millisecond) | ||
}, | ||
} | ||
if err := ssh.AllocatePty()(srv); err != nil { | ||
t.Fatalf("expected no error, got %v", err) | ||
} | ||
|
||
sess := testsession.New(t, srv, nil) | ||
if err := sess.RequestPty("xterm", 500, 200, nil); err != nil { | ||
t.Fatalf("expected no error, got %v", err) | ||
} | ||
|
||
var stdout bytes.Buffer | ||
var stderr bytes.Buffer | ||
sess.Stdout = &stdout | ||
sess.Stderr = &stderr | ||
if err := sess.Run(""); err != nil { | ||
t.Errorf("expected no error, got %v: %s", err, stderr.String()) | ||
} | ||
out := stdout.String() | ||
expectContains(t, out, "hello") | ||
expectContains(t, out, "HELLO=world") | ||
expectContains(t, out, tmp) | ||
} | ||
|
||
func TestCommandPtyError(t *testing.T) { | ||
if runtime.GOOS == "windows" { | ||
t.Skip() | ||
} | ||
srv := &ssh.Server{ | ||
Handler: func(s ssh.Session) { | ||
if err := Command(s, "nopenopenope").Run(); err != nil { | ||
Fatal(s, err) | ||
} | ||
}, | ||
} | ||
if err := ssh.AllocatePty()(srv); err != nil { | ||
t.Fatalf("expected no error, got %v", err) | ||
} | ||
|
||
sess := testsession.New(t, srv, nil) | ||
if err := sess.RequestPty("xterm", 500, 200, nil); err != nil { | ||
t.Fatalf("expected no error, got %v", err) | ||
} | ||
|
||
var stderr bytes.Buffer | ||
sess.Stderr = &stderr | ||
if err := sess.Run(""); err == nil { | ||
t.Errorf("expected an error, got nil") | ||
} | ||
expect := `exec: "nopenopenope"` | ||
if s := stderr.String(); !strings.Contains(s, expect) { | ||
t.Errorf("expected output to contain %q, got %q", expect, s) | ||
} | ||
} | ||
|
||
func runEcho(s ssh.Session, str string) { | ||
cmd := Command(s, "echo", str) | ||
if runtime.GOOS == "windows" { | ||
cmd = Command(s, "cmd", "/C", "echo", str) | ||
} | ||
// these should do nothing... | ||
cmd.SetStderr(nil) | ||
cmd.SetStdin(nil) | ||
cmd.SetStdout(nil) | ||
if err := cmd.Run(); err != nil { | ||
Fatal(s, err) | ||
} | ||
} | ||
|
||
func runEnv(s ssh.Session, env []string) { | ||
cmd := Command(s, "env") | ||
if runtime.GOOS == "windows" { | ||
cmd = Command(s, "cmd", "/C", "set") | ||
} | ||
cmd.SetEnv(env) | ||
if err := cmd.Run(); err != nil { | ||
Fatal(s, err) | ||
} | ||
if len(cmd.Environ()) == 0 { | ||
Fatal(s, "cmd.Environ() should not be empty") | ||
} | ||
} | ||
|
||
func runPwd(s ssh.Session, dir string) { | ||
cmd := Command(s, "pwd") | ||
if runtime.GOOS == "windows" { | ||
cmd = Command(s, "cmd", "/C", "cd") | ||
} | ||
cmd.SetDir(dir) | ||
if err := cmd.Run(); err != nil { | ||
Fatal(s, err) | ||
} | ||
} | ||
|
||
func expectContains(tb testing.TB, s, substr string) { | ||
if !strings.Contains(s, substr) { | ||
tb.Errorf("expected output %q to contain %q", s, substr) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris | ||
// +build darwin dragonfly freebsd linux netbsd openbsd solaris | ||
|
||
package wish | ||
|
||
import "github.com/charmbracelet/ssh" | ||
|
||
func (c *Cmd) doRun(ppty ssh.Pty, _ <-chan ssh.Window) error { | ||
if err := ppty.Start(c.cmd); err != nil { | ||
return err | ||
} | ||
return c.cmd.Wait() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
//go:build windows | ||
// +build windows | ||
|
||
package wish | ||
|
||
import ( | ||
"fmt" | ||
"time" | ||
|
||
"github.com/charmbracelet/ssh" | ||
) | ||
|
||
func (c *Cmd) doRun(ppty ssh.Pty, _ <-chan ssh.Window) error { | ||
if err := ppty.Start(c.cmd); err != nil { | ||
return err | ||
} | ||
|
||
start := time.Now() | ||
for c.cmd.ProcessState == nil { | ||
if time.Since(start) > time.Second*10 { | ||
return fmt.Errorf("could not start process") | ||
} | ||
time.Sleep(100 * time.Millisecond) | ||
} | ||
if !c.cmd.ProcessState.Success() { | ||
return fmt.Errorf("process failed: exit %d", c.cmd.ProcessState.ExitCode()) | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters