Coder Social home page Coder Social logo

wish's Introduction

Wish

A nice rendering of a star, anthropomorphized somewhat by means of a smile, with the words ‘Charm Wish’ next to it
Latest Release GoDoc Build Status Codecov branch Go Report Card

Make SSH apps, just like that! 💫

SSH is an excellent platform for building remotely accessible applications. It offers:

  • secure communication without the hassle of HTTPS certificates
  • user identification with SSH keys
  • access from any terminal

Powerful protocols like Git work over SSH and you can even render TUIs directly over an SSH connection.

Wish is an SSH server with sensible defaults and a collection of middlewares that makes building SSH apps really easy. Wish is built on gliderlabs/ssh and should be easy to integrate into any existing projects.

What are SSH Apps?

Usually, when we think about SSH, we think about remote shell access into servers, most commonly through openssh-server.

That's a perfectly valid (and probably the most common) use of SSH, but it can do so much more than that. Just like HTTP, SMTP, FTP and others, SSH is a protocol! It is a cryptographic network protocol for operating network services securely over an unsecured network. 1

That means, among other things, that we can write custom SSH servers without touching openssh-server, so we can securely do more things than just providing a shell.

Wish is a library that helps writing these kind of apps using Go.

Middleware

Wish middlewares are analogous to those in several HTTP frameworks. They are essentially SSH handlers that you can use to do specific tasks, and then call the next middleware.

Notice that middlewares are composed from first to last, which means the last one is executed first.

Bubble Tea

The bubbletea middleware makes it easy to serve any Bubble Tea application over SSH. Each SSH session will get their own tea.Program with the SSH pty input and output connected. Client window dimension and resize messages are also natively handled by the tea.Program.

You can see a demo of the Wish middleware in action at: ssh git.charm.sh

Git

The git middleware adds git server functionality to any ssh server. It supports repo creation on initial push and custom public key based auth.

This middleware requires that git is installed on the server.

Logging

The logging middleware provides basic connection logging. Connects are logged with the remote address, invoked command, TERM setting, window dimensions and if the auth was public key based. Disconnect will log the remote address and connection duration.

Access Control

Not all applications will support general SSH connections. To restrict access to supported methods, you can use the activeterm middleware to only allow connections with active terminals connected and the accesscontrol middleware that lets you specify allowed commands.

Default Server

Wish includes the ability to easily create an always authenticating default SSH server with automatic server key generation.

Examples

There are examples for a standalone Bubble Tea application and Git server in the examples folder.

Apps Built With Wish

Pro tip

When building various Wish applications locally you can add the following to your ~/.ssh/config to avoid having to clear out localhost entries in your ~/.ssh/known_hosts file:

Host localhost
    UserKnownHostsFile /dev/null

How it works?

Wish uses gliderlabs/ssh to implement its SSH server, and OpenSSH is never used nor needed — you can even uninstall it if you want to.

Incidentally, there's no risk of accidentally sharing a shell because there's no default behavior that does that on Wish.

Running with SystemD

If you want to run a Wish app with systemd, you can create an unit like so:

/etc/systemd/system/myapp.service:

[Unit]
Description=My App
After=network.target

[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/home/myapp/
ExecStart=/usr/bin/myapp
Restart=on-failure

[Install]
WantedBy=multi-user.target

You can tune the values below, and once you're happy with them, you can run:

# need to run this every time you change the unit file
sudo systemctl daemon-reload

# start/restart/stop/etc:
sudo systemctl start myapp

If you use a new user for each app (which is good), you'll need to create them first:

useradd --system --user-group --create-home myapp

That should do it.

Feedback

We’d love to hear your thoughts on this project. Feel free to drop us a note!

License

MIT


Part of Charm.

The Charm logo

Charm热爱开源 • Charm loves open source

Footnotes

  1. https://en.wikipedia.org/wiki/Secure_Shell

wish's People

Contributors

arunsathiya avatar aymanbagabas avatar caarlos0 avatar decentral1se avatar dependabot[bot] avatar dezren39 avatar jamesreprise avatar lindsayzhou avatar maaslalani avatar mdosch avatar meowgorithm avatar muesli avatar toby avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

wish's Issues

Lipgloss doesn't detect background correctly when using wish

Describe the bug
When using the bubbletea.MakeRenderer function, lipgloss.AdaptiveColor and lipgloss.Renderer.HasDarkBackground always assume a black background, even though the terminal emulator from which i am using ssh has set a light background.

Setup
I'm running the wish example locally with go run . and also use a local terminal to access the ssh server.
It happens on Fedora 40 and MacOS with Gnome Terminal, iTerm2 and the standard mac terminal (all using zsh).

To Reproduce

  1. Use the bubbletea example from the examples folder
  2. Log HasDarkBackground using
    log.Info(renderer.HasDarkBackground()) in the teaHandler function right after creating the renderer using the bubbletea middleware
  3. Create a new Lipgloss Style in the teaHandler function that uses the renderer created using the bubbletea middleware and lipgloss.AdaptiveColor
    3.1 Render some text in the View() method of the model using the adaptive color lipgloss style

Source Code
I've created a simple one-file example in this gist:
https://gist.github.com/Aerochrome/5fd2af09f94eda22a0875068b8824acc

Expected behavior
When accessing with a terminal emulator with a light background, the second line should be colored pink.
When accessing with a dark background, the second line should be colored red.

Screenshots
Expectation:
gist_working

Reality:
gist_not_working

Additional context
DISCLAIMER: I am very new to TUIs and fairly new to go, so i'm not sure how useful my debugging findings actually are, but i want to share them nonetheless in case they're of any use.

I suspect that this issue might be related to a missing Fd() method implementation on the ssh.Session struct.
In the bubbletea.newRenderer method in this file a check is performed on whether the Slave pointer is nil. If so, it passes the session struct into lipgloss.NewRenderer.

While this struct has a Write method, it doesn't seem to implement the Fd method.

When adding a very rudimentary one like so, the background color check seems to work properly and produces a working example.

func (sess *session) Fd() uintptr {
    return sess.pty.Slave.Fd() // Also works if sess.pty.Slave is nil
}

Of course, this doesn't account for sess.pty.Slave being nil (even though it produces a working example even if it is nil and i'm unsure why).

This suspicion is further confirmed by looking at an older wish example in the lipgloss repository, where a "helper" struct is created to pass into lipgloss.Renderer.SetOutput which also implements Write and Fd.
It also implements Read, though it doesn't seem to influence the outcome of a working example in my case.

So i've tried to use this helper struct, setting tty to pty.Slave and passing it into bubbletea.MakeRenderer like so, which produces a working example.

sessionBridge := &sshOutput{
    Session: s,
    tty: pty.Slave,
}

renderer := bubbletea.MakeRenderer(sessionBridge)

Again, this also works if pty.Slave is nil.

It is also worth mentioning that in my case checking pty.Master and pty.Slave against nil in the teaHandler function returns true in both cases, even though s.Pty() returns true as third return value which i assume means that a PTY was accepted for this session.
I don't know if this is expected behavior in this specific case?

log.Info(pty.Master == nil)
log.Info(pty.Slave == nil)

chat application

Hey there!

Last year I wrote a game with Wish and Bubbletea. I had some game state shared between players, but I failed to find a way to have events sent from one client to another. Any tips on where to start? Would a minimal chat application make a good example for the wish repository?

banner instead of comment?

is it possible to make a banner middleware? i'm just gonna have my tea middleware output a banner instead but i had tried to figure out a way to make a generic middleware that is basically comment but at the beginning, couldn't figure it out! kind of feels like it would need to be a (ssh.Option) wish.WithBanner(string) function like outside middleware so the server can just output first. basically for showing a motd and standard banner about terms of service type stuff. later i'll probably build a fancier manual auth for users where they, idk, hit 'Y' or 'Enter' to accept or Ctrl+c to decline, but i'd like to have an easy way to throw a generic header for simpler cases or maybe when user is recognized as already having accepted.

scp limited fs

i wish i could make like, an fs with a limited file system size for use with scp.

like, using ratelimiter for bandwidth/burst related limiting, but something that would let me provide a directory and they can put as much as they want until the dir (and anything in it) is more than limit amount. if a put would be more than amount then error.

is there a simple way to make this? so that users can scp, idk, up to 2gb, but any more breaks

Support for links?

There is a standard called OSC 8 that some modern terminal emulators use to enable clickable links. This is not universally supported, but it does work in a number of popular terminals like iTerm2, GNOME Terminal, mintty, and others.

Here is an example of how you can output a hyperlink in a terminal using this method:

echo -e '\e]8;;http://example.com\aThis is a link\e]8;;\a'

When you run this command in a terminal that supports OSC 8, it will output the text "This is a link", and you can click on it to open http://example.com/ in your web browser.

While my terminal supports OSC 8, the link is not clickable when output with Wish.

fmt.Fprintf(&b, "%s\n", "\033]8;;http://example.com\aThis is a link\033]8;;\a")

Do you happen to know if this is a limitation of ssh or wish or something else?

Given that I can ssh into a machine and use the echo command and it produces a clickable link, and I can also write a simple program in Go that also produces the clickable link I'm inclined to believe that it is a limitation of wish, isn't it?

package main

import "fmt"

func main() {
    fmt.Println("\033]8;;http://example.com\aThis is a link\033]8;;\a")
}

refresh authorized_keys

should we behave more like the openssh server?

if yes, we should probably re-read the authorized_keys files eventually (on new auth try? timed? on fs events?)

right now, it needs a server restart...

undefined: wish.Session

I was trying to run a couple of the examples in this repo, but seems they are broken after a recent refactor (ssh.Session => wish.Session)

./main.go:71:28: undefined: wish.Session

WithCustomAuth

hello!

so i want to accept any session, but then run a custom function that is basically WithCustomAuth func(h ssh.Handler) bool, or a custom handler that only has a context like func(ctx Context) bool or both (context would work especially well if i could guarantee customAuth always runs and is last, so i could maybe pass data through ctx.Permissions from the pk/interactive/pw handlers and then handle the last part there.

maybe this isn't the right way to handle this. right now i have a middleware where i have something like

func(h ssh.Handler) ssh.Handler {
	return func(s ssh.Session) {
		authorizedKey := gossh.MarshalAuthorizedKey(s.PublicKey())
...

and intend to allow it to handle password/interactive auth too. i have each withAuth before this, so 'all auth' is accepted and then i handle the 'real auth' here. my concern is that as a middleware i don't know that i have it in the right order. so i have ssh logic in this middleware, but i also have the scp middleware on the server. my belief is that the scp middleware doesn't run the ssh handler above, or rather doesn't prevent scp i can't use it as auth. so i need to split my ssh handler into the auth portion and the real ssh stuff, then i need a way to run custom auth which is generic and functions for all the varied middleware? does that make sense? idk. I can just replicate the logic for each of the 3 auth paths? maybe its just a matter of how i order the middleware list and it's all covered already?

ratelimiter how does it work?

I'm sorry I don't understand what a token is or a request? Is it like amount of data? Or amount of commands? Or ssh connections? I understand the lru and the ip address part, and conceptually the token thing, but I don't understand how to control this interface or what it exactly means. I tried looking but couldn't find out.

SSH Reverse Tunnel?

Hello, came across Wish and played around with some examples, it looks great. Would it support SSH Reverse Tunneling?

Issue with Session PublicKey Availability and Client Access in Wish

Hello charmbracelet/wish team,

I've been working on a project where I initially developed a login form that validates username and password against a single-sign-on server. To enhance the user experience by reducing the need for frequent logins, I stored the session.PublicKey() for automatic authentication post a successful login, as per the example in your documentation.

However, I encountered an issue where session.PublicKey() was returning empty. To resolve this, I added the following:

wish.WithPublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
    return true
}),

This change successfully allowed me to access the public key, but it introduced a new problem. Now, clients without a public key cannot connect, receiving a Permission denied (publickey) error.

Is there a way to configure the system so that I can both access the public key for users who have it, and still allow clients who don't support public keys to connect? Ideally, I want to support both types of clients without compromising the convenience of automatic authentication for those who can use public keys.

Any guidance or suggestions you can provide would be greatly appreciated!

huh? middleware?

it should be possible to run it wrapped into another model, but it would be nice to have a simple way to "just serve this form for me".

Scp example requires -O flag with openssh 9.0

Greetings!

Love the library and everything you all are working towards!

openssh v9 just released which changes the default protocol for scp https://www.openssh.com/txt/release-9.0

The current scp example does not work unless specifying the -O flag.

It seems like anyone that wishes to implement an scp server now needs to support both: legacy scp and sftp.

I wanted to surface that potential issue here as well as ask a general question about implementation.

I'm probably going to start working on adding sftp support to scp but wondered if you all had any advice on how to best implement it? Happy to send whatever I come up with upstream.

Thanks!

Directory Traversal

Repo paths are not properly sanitized so users can access repos outside the designated repo dir.

Users with read access will be able to access repos outside the directory, and users with write permissions have the ability to write outside that directory as well.

A quick example that sort of demonstrates whats happening is just to run

git clone 'ssh://git.charm.sh/soft-serve/../glow'

That example doesnt demonstrate reading outside the repo directory (because I dont know where there may be repos on that server) but it does show that it walks the directory path just fine including the ... An attacker could implement some scanning logic to find other repos they arent supposed to have access to.

Color issues when running the program under a systemd service

Hi!

First, thanks for the great framework to make pretty terminal applications :)

I am currently working on a SSH application, using Wish and BubbleTea. In particular, I am using somewhere a colored progress bar. This is working well.

To manage and start properly my application, I am using a simple systemd service:

[Unit]
Description=My SSH application
After=network.target

[Service]
ExecStart=/path/to/binary
WorkingDirectory=/path/to/
Restart=on-failure
RestartSec=15s

[Install]
WantedBy=multi-user.target

And when I start this service, the SSH server is working well, but my terminal has no color, all is black and white. This happens only when I run the application through the systemd service. When I manually launch the binary, the colors are well displayed.

Do you know the origin of the issue, and how to fix it? I don't really want to keep my application opened in a tmux :D

hooks

Hi 👋🏼 — any chance you would consider supporting git hooks in the git middleware? I saw soft-serve has them, but it does too many other things I would have to turn off, etc.. And this server has been a pleasure so far. I think the only thing I am missing is being able to override ensureRepo() somehow to add the hooks?

scp windows

so scp with windows for server and client works, but there are some specific interesting things that may need changes to be 'seamless'.

  • i get protocol error: expected control record on download, this sounds like it is server side somewhere
    • for my simple test cases so far, the download still succeeds.
    • upload didn't show this error and it uploaded fine. i had a slight delay in seeing the upload appear in the mounted folder but that might have been a graphical ui bug on my machine
  • for any conn to work, i need to specify -O on the client (PowerShell/Win32-OpenSSH#1945 (comment)) which is (https://man7.org/linux/man-pages/man1/scp.1.html)
Use the legacy SCP protocol for file transfers instead of
the SFTP protocol.  Forcing the use of the SCP protocol
may be necessary for servers that do not implement SFTP,
for backwards-compatibility for particular filename
wildcard patterns and for expanding paths with a ‘~’
prefix for older SFTP servers.

system info

 scoop which scp 
C:\WINDOWS\System32\OpenSSH\scp.exe
 scoop which ssh 
C:\WINDOWS\System32\OpenSSH\ssh.exe
 ssh -V
OpenSSH_for_Windows_9.2p1, LibreSSL 3.6.2
 ((Get-Item (Get-Command sshd).Source).VersionInfo.FileVersion)
9.2.3.1
 ((Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows nt\CurrentVersion\" -Name ProductName).ProductName)
Windows 10 Pro

pty + bubbletea on windows

I suspect there's somewhere a go io.Copy that needs to be made cancelable.

If you run the wish-exec example on a Windows machine, SSH into it, and press s, e, or q, you'll notice that especially the first couple of keypresses seem to be ignored.

Don't know where this problem is, but gonna investigate.

Reverse tunnel example problem?

Hello, thanks so much for the forward example added in #191. I'm trying that but can't get it to work. Probbaly I'm misunderstanding something. Here's what i do:

I run the Wish simple example on port 23235 and test that works:

go run main.go
2024/02/25 13:44:26 INFO Starting SSH server host=localhost port=23235
2024/02/25 13:45:04 INFO tornt connect 127.0.0.1:55671 false [] xterm-256color 94 6
2024/02/25 13:45:04 INFO 127.0.0.1:55671 disconnect 0s
ssh localhost -p 23235
Hello, world!
Connection to localhost closed.

Then in another terminal window i run the Wish forward example:

go run main.go
2024/02/25 13:52:16 INFO Starting SSH server host=localhost port=23234
2024/02/25 13:52:26 INFO reverse port forwarding allowed host=localhost port=23236

and in another terminal window run the ssh tunnel:

ssh -N -R 23236:localhost:23235 -p 23234 localhost

then back at the ssh clinet window try to access the simple example via the tunnel using port 23236

ssh localhost -p 23236
kex_exchange_identification: read: Connection reset
Connection reset by 127.0.0.1 port 23236

which doesn't work and the connection is just reset.

What am i doing wrong?

Bubbletea ExecProcess within a wish session

I have a bubble tea application where users can invoke an ExecProcess command to do something like enter vim. When I host the application using wish, the execprocess functionality breaks. For example:

package main

// An example Bubble Tea server. This will put an ssh session into alt screen
// and continually print up to date terminal information.

import (
	"context"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"os/signal"
	"syscall"
	"time"

	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/log"
	"github.com/charmbracelet/ssh"
	"github.com/charmbracelet/wish"
	bm "github.com/charmbracelet/wish/bubbletea"
	lm "github.com/charmbracelet/wish/logging"
)

const (
	host = "localhost"
	port = 23234
)

func main() {
	s, err := wish.NewServer(
		wish.WithAddress(fmt.Sprintf("%s:%d", host, port)),
		wish.WithMiddleware(
			bm.Middleware(teaHandler),
			lm.Middleware(),
		),
	)
	if err != nil {
		log.Error("could not start server", "error", err)
	}

	done := make(chan os.Signal, 1)
	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
	log.Info("Starting SSH server", "host", host, "port", port)
	go func() {
		if err = s.ListenAndServe(); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
			log.Error("could not start server", "error", err)
			done <- nil
		}
	}()

	<-done
	log.Info("Stopping SSH server")
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer func() { cancel() }()
	if err := s.Shutdown(ctx); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
		log.Error("could not stop server", "error", err)
	}
}

// You can wire any Bubble Tea model up to the middleware with a function that
// handles the incoming ssh.Session. Here we just grab the terminal info and
// pass it to the new model. You can also return tea.ProgramOptions (such as
// tea.WithAltScreen) on a session by session basis.
func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) {
	pty, _, active := s.Pty()
	if !active {
		wish.Fatalln(s, "no active terminal, skipping")
		return nil, nil
	}
	m := model{
		term:   pty.Term,
		width:  pty.Window.Width,
		height: pty.Window.Height,
	}
	return m, []tea.ProgramOption{tea.WithAltScreen()}
}

// Just a generic tea.Model to demo terminal information of ssh.
type model struct {
	term   string
	width  int
	height int
}

func (m model) Init() tea.Cmd {
	return nil
}

type VimFinishedMsg struct{ err error }

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.WindowSizeMsg:
		m.height = msg.Height
		m.width = msg.Width
	case tea.KeyMsg:
		switch msg.String() {
		case "e":
			c := exec.Command("vim", "file.txt")
			cmd := tea.ExecProcess(c, func(err error) tea.Msg {
				return VimFinishedMsg{err: err}
			})
			return m, cmd
		case "q", "ctrl+c":
			return m, tea.Quit
		}
	}
	return m, nil
}

func (m model) View() string {
	s := "Your term is %s\n"
	s += "Your window size is x: %d y: %d\n\n"
	s += "Press 'q' to quit\n"
	return fmt.Sprintf(s, m.term, m.width, m.height)
}

If I don't run this with wish, e.g. replace main with the following contents:

	if _, err := tea.NewProgram(model{}).Run(); err != nil {
		fmt.Println("uh oh:", err)
		os.Exit(1)
	}

Then the exec process works fine.

Any ideas on if and how this might be possible to fix?

String together multiple tea.Programs inside one Wish application?

Maybe my thinking is wrong, if so, please correct me.

I would like to have two, and perhaps more tea.Programs inside of a Wish app.

For example, if public key authentication fails, I would like to fall back to an email access code base authentication, that would have a UI substantially different from the rest of my app.

I think right now my only choice is to add a case inside my func (m model) View() string and either render the access code authenticator UI or render the rest of the app.

I look into func myCustomBubbleteaMiddleware() wish.Middleware:

func myCustomBubbleteaMiddleware() wish.Middleware {

However, inside myCustomBubbleteaMiddleware when building the teaHandler (to use the variable name from examples/bubbleteaprogram) I don't have access to the ssh.Session to check if I should instantiate the tea.Program for the emailed authentication code, or if the SSH key could be validated and thus the tea.Program for the rest of the app should be instantiated.

Also, even if I did succeed in initially correctly instantiate one of two teaHandlers, once that tea.Program successfully completed, I don't see any way the running teaHandler could be replaced by another teaHandler.

Ultimately I think the question is really your recommendation on organizing multi-screen tea applications, especially cases where the header/footer may be different on some screens (initial screens, graphs, final screens).

If a more complicated application could be built by combining smaller tea applications, seems like it could really simplify development, testing and maintenance.

Max pseudo terminal limit in linux

Is it true that wish (or gliderlabs/ssh) allocates a new pty for each session? If so, doesn't that mean it's possible for the server to hit the max pty limit specified in /proc/sys/kernel/pty/max if there are a lot of concurrent connections? I imagine this would've happened when charm.sh hit the HN frontpage. I'm just wondering if this is something that is possible or if i'm misunderstanding how it works.

access to the `tea.Program` object

I'm using wish/bubbletea to serve an application. I would like to use the method Program.Send so another part of the application can trigger a behaviour in the UI part, but I don't see how I can access the tea.Program object from the middleware.

fail2ban middleware

would be nice to have a fail2ban and/or some sort of rate limit middleware

terminal not restored properly

When the connection gets closed by the remote host, the terminal gets not restored properly. e.g. the cursor is missing

Suggestions on how to host an app written with wish

The problem

I want to deploy the app to my domain so that it's accessible from the terminal via ssh antoni.ai, but I have some difficulties figuring out how to do it properly.

First, my go-to easy hosting provider (Railway) doesn't let ssh protocol through, so I can't deploy there. I've considered just getting a compute service like a droplet on digital ocean, but I also need to ssh into it to control it, so I can't expose port 22 there.

My best guess here is to just have a service running and listening on a different port, and then use a reverse proxy to point port 22 to the previously mentioned service, but I once again hit the issue of having the 22nd port occupied for controlling the service.

I also don't want to use my raspberry pi for this, because I don't want people ssh'ing into my home network, even if it looks pretty safe behind the TUI.

Any suggestions would be greatly appreciated! ❤️

wish-exec has issues starting a bash shell (outputs to server stdout | error `Inappropriate ioctl for device`)

What

The wish-exec example works fine with vim but when trying to exec an interactive bash process the following occurs.

Outputs to server Stdout

Relying on bubbletea.ExecProcess to set the default Std(in,out,err) for the *exec.Cmd results in the Stdout of the bash process being on the server's stdout.

https://github.com/charmbracelet/bubbletea/blob/v0.25.0/exec.go#L112C16-L112C22
https://github.com/charmbracelet/bubbletea/blob/v0.25.0/options.go#L35C23-L35C32
https://github.com/muesli/termenv/blob/v0.15.2/output.go#L184

Output to client Stdout but bash reports an error at startup

Explicitly setting the Std(in,out,err) to the pty.Slave, as the makeOptions function does results in the bash process outputting to the client but displaying the following error upon startup.

https://github.com/charmbracelet/wish/blob/main/bubbletea/tea_unix.go#L22

bash: cannot set terminal process group (1632577): Inappropriate ioctl for device
bash: no job control in this shell

Ctrl+c isn't registered by shell either.

Reproduction

The following is a diff with a runnable example of this error.

diff --git a/examples/wish-exec/main.go b/examples/wish-exec/main.go
index 1137779..1dbbdcd 100644
--- a/examples/wish-exec/main.go
+++ b/examples/wish-exec/main.go
@@ -93,6 +93,32 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 				return vimFinishedMsg{err: err}
 			})
 			return m, cmd
+		case "f":
+			log.Info("pressed f: launching bash (server stdout)")
+			// Output shows up on server's stdout, no error messages
+			c := exec.Command("bash", "-il")
+			cmd := tea.ExecProcess(c, func(err error) tea.Msg {
+				if err != nil {
+					log.Error("bash (server stdout) finished", "error", err)
+				}
+				return vimFinishedMsg{err: err}
+			})
+			return m, cmd
+		case "b":
+			log.Info("pressed b: launching bash (client stdout)")
+			pty, _, _ := m.sess.Pty()
+			// Output shows up on client stdout, but there is an error message
+			c := exec.Command("bash", "-il")
+			c.Stdin = pty.Slave
+			c.Stdout = pty.Slave
+			c.Stderr = pty.Slave
+			cmd := tea.ExecProcess(c, func(err error) tea.Msg {
+				if err != nil {
+					log.Error("bash (client stdout) finished", "error", err)
+				}
+				return vimFinishedMsg{err: err}
+			})
+			return m, cmd
 		case "q", "ctrl+c":
 			return m, tea.Quit
 		}

Peek 2024-01-19 20-18

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.