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

feat: go back to the list after disconnecting #52

Merged
merged 45 commits into from Apr 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
2a2fffe
feat: allow to set the items to the list element
caarlos0 Apr 4, 2022
cd72b93
fix: lint issues
caarlos0 Apr 4, 2022
3958761
fix: deprecations et al
caarlos0 Apr 4, 2022
d49b60e
test: fixes
caarlos0 Apr 4, 2022
fefecc4
feat: go back to the list after disconnecting
caarlos0 Apr 5, 2022
b170c35
refactor: small improvement
caarlos0 Apr 5, 2022
fc3f356
fix: dep
caarlos0 Apr 5, 2022
132d709
fix: remote back to list
caarlos0 Apr 5, 2022
47a99d9
fix: stdin
caarlos0 Apr 5, 2022
cf27ea1
chore: cleanup
caarlos0 Apr 5, 2022
6c79753
chore: spacing
caarlos0 Apr 5, 2022
67c9a21
fix: cleanup
caarlos0 Apr 5, 2022
43f427b
fix: -t appname
caarlos0 Apr 5, 2022
19c8030
Merge branch 'main' into back-to-the-list
caarlos0 Apr 6, 2022
0ad98d7
fix: merge issues
caarlos0 Apr 6, 2022
c520e46
fix: ssh -t hanging
caarlos0 Apr 6, 2022
b122f56
doc: godoc
caarlos0 Apr 6, 2022
7a995ee
Merge branch 'main' into back-to-the-list
caarlos0 Apr 6, 2022
7f71328
chore: go mod tidy
caarlos0 Apr 6, 2022
1410577
refactor: moved some code around
caarlos0 Apr 7, 2022
115e3e0
chore: fmt
caarlos0 Apr 7, 2022
92c686a
chore: handle errors a bit better
caarlos0 Apr 9, 2022
3df8e29
fix: local back from list missing first keypress
caarlos0 Apr 11, 2022
0e627a0
fix: pwd et al
caarlos0 Apr 11, 2022
fc60cf8
fix: prevent interacting with list on error view
caarlos0 Apr 11, 2022
1edae4f
fix: unused prop
caarlos0 Apr 11, 2022
f1c2025
refactor: clients
caarlos0 Apr 12, 2022
64a2eb5
fix: better diffs
caarlos0 Apr 12, 2022
f9f7b53
fix: gitinore
caarlos0 Apr 12, 2022
4c5f852
Revert "fix: gitinore"
caarlos0 Apr 12, 2022
03befaa
fix: diffs
caarlos0 Apr 12, 2022
7144197
fix: out/err/i
caarlos0 Apr 12, 2022
e5e3a0f
fix: diffs
caarlos0 Apr 12, 2022
008783b
fix: rm files
caarlos0 Apr 12, 2022
c8bcfa4
fix: typo
caarlos0 Apr 12, 2022
2877621
fix: reset pty
caarlos0 Apr 12, 2022
42b10a3
chore: todo
caarlos0 Apr 12, 2022
3a4da79
refactor: improve NewListing
caarlos0 Apr 12, 2022
f0b5279
fix: rm dead code
caarlos0 Apr 12, 2022
3eb0621
fix: quit cleaner
caarlos0 Apr 12, 2022
12dfe9a
fix: rm replaces
caarlos0 Apr 12, 2022
70342e2
fix: move the resetPty around
caarlos0 Apr 12, 2022
e3efe4f
fix: ssh agent check
caarlos0 Apr 12, 2022
de128d0
fix: typo
caarlos0 Apr 12, 2022
f3f730a
test: fix
caarlos0 Apr 13, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 7 additions & 6 deletions blocking_reader.go → blocking/reader.go
@@ -1,31 +1,32 @@
package wishlist
package blocking

import (
"io"
"time"
)

// blockingReader is an io.Reader that blocks until the underlying reader
// Reader is an io.Reader that blocks until the underlying reader until
// returns something other than io.EOF.
//
// on EOF, it'll keep trying to read every 10ms.
//
// The purpose of this is to be used to "emulate a STDIN" (which never EOFs)
// from another io.Reader, e.g. a bytes.Buffer.
type blockingReader struct {
type Reader struct {
r io.Reader
}

func newBlockingReader(r io.Reader) io.Reader {
return blockingReader{r}
// New wraps a given io.Reader into a BlockingReader
func New(r io.Reader) Reader {
return Reader{r: r}
}

type readResult struct {
n int
e error
}

func (r blockingReader) Read(data []byte) (int, error) {
func (r Reader) Read(data []byte) (int, error) {
readch := make(chan readResult, 1)

go func() {
Expand Down
4 changes: 2 additions & 2 deletions blocking_reader_test.go → blocking/reader_test.go
@@ -1,4 +1,4 @@
package wishlist
package blocking

import (
"io"
Expand All @@ -9,7 +9,7 @@ import (
)

func TestBlockingReader(t *testing.T) {
r := newBlockingReader(&testReader{
r := New(&testReader{
results: []readResult{
{n: 0, e: io.EOF}, // EOF should be ignored
{n: 12, e: nil}, // return normally
Expand Down
10 changes: 9 additions & 1 deletion client.go
Expand Up @@ -6,12 +6,14 @@ import (
"io"
"log"

tea "github.com/charmbracelet/bubbletea"
"github.com/muesli/termenv"
gossh "golang.org/x/crypto/ssh"
)

// SSHClient is a SSH client.
type SSHClient interface {
Connect(e *Endpoint) error
For(e *Endpoint) tea.ExecCommand
}

func createSession(conf *gossh.ClientConfig, e *Endpoint) (*gossh.Session, *gossh.Client, closers, error) {
Expand Down Expand Up @@ -72,3 +74,9 @@ func firstNonEmpty(ss ...string) string {
}
return ""
}

func resetPty(w io.Writer) {
fmt.Fprint(w, termenv.CSI+termenv.ExitAltScreenSeq)
fmt.Fprint(w, termenv.CSI+termenv.ResetSeq+"m")
fmt.Fprintf(w, termenv.CSI+termenv.EraseDisplaySeq, 2) // nolint:gomnd
}
5 changes: 3 additions & 2 deletions client_auth.go
Expand Up @@ -23,6 +23,7 @@ var errNoRemoteAgent = fmt.Errorf("no agent forwarded")
//
// it first tries to use ssh-agent, if that's not available, it creates and uses a new key pair.
func remoteBestAuthMethod(s ssh.Session) (gossh.AuthMethod, agent.Agent, closers, error) {
// TODO: we should probably make password protected keys work here too
method, agt, cls, err := tryRemoteAuthAgent(s)
if err != nil {
return nil, nil, nil, err
Expand All @@ -47,7 +48,7 @@ func remoteBestAuthMethod(s ssh.Session) (gossh.AuthMethod, agent.Agent, closers
// It'll return a nil list if none of the methods is available.
func localBestAuthMethod(e *Endpoint) ([]gossh.AuthMethod, error) {
var methods []gossh.AuthMethod
if method, err := tryLocalAgent(); err != nil || method != nil {
if method, err := tryLocalAgent(); err == nil && method != nil {
methods = append(methods, method)
}

Expand Down Expand Up @@ -80,7 +81,7 @@ func getLocalAgent() (agent.Agent, error) {
}
conn, err := net.Dial("unix", socket)
if err != nil {
return nil, fmt.Errorf("failed to connecto to SSH_AUTH_SOCK: %w", err)
return nil, fmt.Errorf("failed to connect to SSH_AUTH_SOCK: %w", err)
}
return agent.NewClient(conn), nil
}
Expand Down
69 changes: 56 additions & 13 deletions client_local.go
Expand Up @@ -3,11 +3,14 @@ package wishlist
import (
"context"
"fmt"
"io"
"log"
"os"
"os/user"
"path/filepath"

tea "github.com/charmbracelet/bubbletea"
"github.com/muesli/cancelreader"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
"golang.org/x/term"
Expand All @@ -20,33 +23,73 @@ func NewLocalSSHClient() SSHClient {

type localClient struct{}

func (c *localClient) Connect(e *Endpoint) error {
func (c *localClient) For(e *Endpoint) tea.ExecCommand {
return &localSession{
endpoint: e,
}
}

type localSession struct {
// endpoint we are connecting to
endpoint *Endpoint

stdin io.Reader
stdout, stderr io.Writer
}

func (s *localSession) SetStdin(r io.Reader) {
s.stdin = r
}

func (s *localSession) SetStdout(w io.Writer) {
s.stdout = w
}

func (s *localSession) SetStderr(w io.Writer) {
s.stderr = w
}

func (s *localSession) Run() error {
resetPty(s.stdout)

user, err := user.Current()
if err != nil {
return fmt.Errorf("failed to get current username: %w", err)
}

methods, err := localBestAuthMethod(e)
methods, err := localBestAuthMethod(s.endpoint)
if err != nil {
return fmt.Errorf("failed to setup a authentication method: %w", err)
}

conf := &ssh.ClientConfig{
User: firstNonEmpty(e.User, user.Username),
User: firstNonEmpty(s.endpoint.User, user.Username),
Auth: methods,
HostKeyCallback: hostKeyCallback(e, filepath.Join(user.HomeDir, ".ssh/known_hosts")),
HostKeyCallback: hostKeyCallback(s.endpoint, filepath.Join(user.HomeDir, ".ssh/known_hosts")),
}

session, client, cls, err := createSession(conf, e)
session, client, cls, err := createSession(conf, s.endpoint)
defer cls.close()
if err != nil {
return fmt.Errorf("failed to create session: %w", err)
}
defer closers{func() error {
rc, ok := session.Stdin.(cancelreader.CancelReader)
if ok && !rc.Cancel() {
return fmt.Errorf("could not cancel reader")
}
return nil
}}.close()

session.Stdout = os.Stdout
session.Stderr = os.Stderr
session.Stdin = os.Stdin
session.Stdout = s.stdout
session.Stderr = s.stderr
stdin, err := cancelreader.NewReader(s.stdin)
if err != nil {
return fmt.Errorf("could not create cancel reader")
}
session.Stdin = stdin

if e.ForwardAgent {
if s.endpoint.ForwardAgent {
log.Println("forwarding SSH agent")
agt, err := getLocalAgent()
if err != nil {
Expand All @@ -63,7 +106,7 @@ func (c *localClient) Connect(e *Endpoint) error {
}
}

if e.RequestTTY || e.RemoteCommand == "" {
if s.endpoint.RequestTTY || s.endpoint.RemoteCommand == "" {
fd := int(os.Stdout.Fd())
if !term.IsTerminal(fd) {
return fmt.Errorf("requested a TTY, but current session is not TTY, aborting")
Expand Down Expand Up @@ -92,13 +135,13 @@ func (c *localClient) Connect(e *Endpoint) error {

ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go c.notifyWindowChanges(ctx, session)
go s.notifyWindowChanges(ctx, session)
} else {
log.Println("did not request a tty")
}

if e.RemoteCommand == "" {
if s.endpoint.RemoteCommand == "" {
return shellAndWait(session)
}
return runAndWait(session, e.RemoteCommand)
return runAndWait(session, s.endpoint.RemoteCommand)
}
81 changes: 54 additions & 27 deletions client_remote.go
Expand Up @@ -5,45 +5,78 @@ import (
"io"
"log"

tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/wishlist/blocking"
"github.com/gliderlabs/ssh"
"github.com/muesli/termenv"
gossh "golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
)

type remoteClient struct {
// parent session
session ssh.Session
stdin io.Reader

// stdin, which is usually multiplexed from the session stdin
stdin io.Reader

// whether to exhaust stdin first or not.
// if coming from the list, youll want to do that, otherwise you likely
// dont, as it might hang the connection waiting for something to read.
exhaust bool
}

func (c *remoteClient) For(e *Endpoint) tea.ExecCommand {
if c.exhaust {
_, _ = io.ReadAll(c.stdin)
}
return &remoteSession{
endpoint: e,
parentSession: c.session,
stdin: blocking.New(c.stdin),
}
}

func (c *remoteClient) Connect(e *Endpoint) error {
resetPty(c.session)
type remoteSession struct {
// endpoint we are connecting to
endpoint *Endpoint

method, agt, closers, err := remoteBestAuthMethod(c.session)
defer closers.close()
// the parent session (ie the session running the listing)
parentSession ssh.Session

stdin io.Reader
}

func (s *remoteSession) SetStdin(r io.Reader) {}
func (s *remoteSession) SetStdout(w io.Writer) {}
func (s *remoteSession) SetStderr(w io.Writer) {}

func (s *remoteSession) Run() error {
resetPty(s.parentSession)

method, agt, closers, err := remoteBestAuthMethod(s.parentSession)
if err != nil {
return fmt.Errorf("failed to find an auth method: %w", err)
}
defer closers.close()

conf := &gossh.ClientConfig{
User: firstNonEmpty(e.User, c.session.User()),
HostKeyCallback: hostKeyCallback(e, ".wishlist/known_hosts"),
User: firstNonEmpty(s.endpoint.User, s.parentSession.User()),
HostKeyCallback: hostKeyCallback(s.endpoint, ".wishlist/known_hosts"),
Auth: []gossh.AuthMethod{method},
}

session, client, cl, err := createSession(conf, e)
session, client, cl, err := createSession(conf, s.endpoint)
defer cl.close()
if err != nil {
return fmt.Errorf("failed to create session: %w", err)
}

log.Printf("%s connect to %q, %s", c.session.User(), e.Name, c.session.RemoteAddr().String())
log.Printf("%s connect to %q, %s", s.parentSession.User(), s.endpoint.Name, s.parentSession.RemoteAddr().String())

session.Stdout = c.session
session.Stderr = c.session.Stderr()
session.Stdin = c.stdin
session.Stdout = s.parentSession
session.Stderr = s.parentSession.Stderr()
session.Stdin = s.stdin

if e.ForwardAgent {
if s.endpoint.ForwardAgent {
log.Println("forwarding SSH agent")
if agt == nil {
return fmt.Errorf("requested ForwardAgent, but no agent is available")
Expand All @@ -56,9 +89,9 @@ func (c *remoteClient) Connect(e *Endpoint) error {
}
}

if e.RemoteCommand == "" || e.RequestTTY {
if s.endpoint.RemoteCommand == "" || s.endpoint.RequestTTY {
log.Println("requesting tty")
pty, winch, ok := c.session.Pty()
pty, winch, ok := s.parentSession.Pty()
if !ok {
return fmt.Errorf("requested a tty, but current session doesn't allow one")
}
Expand All @@ -69,16 +102,16 @@ func (c *remoteClient) Connect(e *Endpoint) error {

done := make(chan bool, 1)
defer func() { done <- true }()
go c.notifyWindowChanges(session, done, winch)
go s.notifyWindowChanges(session, done, winch)
}

if e.RemoteCommand == "" {
if s.endpoint.RemoteCommand == "" {
return shellAndWait(session)
}
return runAndWait(session, e.RemoteCommand)
return runAndWait(session, s.endpoint.RemoteCommand)
}

func (c *remoteClient) notifyWindowChanges(session *gossh.Session, done <-chan bool, winch <-chan ssh.Window) {
func (s *remoteSession) notifyWindowChanges(session *gossh.Session, done <-chan bool, winch <-chan ssh.Window) {
for {
select {
case <-done:
Expand All @@ -95,9 +128,3 @@ func (c *remoteClient) notifyWindowChanges(session *gossh.Session, done <-chan b
}
}
}

func resetPty(w io.Writer) {
fmt.Fprint(w, termenv.CSI+termenv.ExitAltScreenSeq)
fmt.Fprint(w, termenv.CSI+termenv.ResetSeq+"m")
fmt.Fprintf(w, termenv.CSI+termenv.EraseDisplaySeq, 2) // nolint:gomnd
}
2 changes: 1 addition & 1 deletion client_signals_unix.go
Expand Up @@ -14,7 +14,7 @@ import (
"golang.org/x/term"
)

func (c *localClient) notifyWindowChanges(ctx context.Context, session *ssh.Session) {
func (s *localSession) notifyWindowChanges(ctx context.Context, session *ssh.Session) {
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGWINCH)
defer func() {
Expand Down
3 changes: 2 additions & 1 deletion client_signals_windows.go
Expand Up @@ -5,8 +5,9 @@ package wishlist

import (
"context"

"golang.org/x/crypto/ssh"
)

// not available because windows does not implement siscall.SIGWINCH.
func (c *localClient) notifyWindowChanges(ctx context.Context, session *ssh.Session) {}
func (c *localSession) notifyWindowChanges(ctx context.Context, session *ssh.Session) {}