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

Do not exit on EOF #45

Merged
merged 2 commits into from Jan 5, 2017
Merged

Do not exit on EOF #45

merged 2 commits into from Jan 5, 2017

Conversation

gurjeet
Copy link
Contributor

@gurjeet gurjeet commented Nov 17, 2016

Forward the EOF to GoTTY, and let the server-side decide if it wants to
terminate the connection. The server closes the connection, and in
response we terminate the readLoop which in turn signals writeLoop to
terminate via the QuitChan.

This allows for the user to pipe commands to gotty-client, and capture
all the result sent by the server. For eg. when gotty is launched as
gotty -w bash, the following command would now wait to capture all
output from the server.

for (( i = 0 ; i < 2; ++i )); do echo echo $i; echo sleep 2; done | ./gotty-client https://gotty.example.com

Before this patch, gotty-client used to exit on encountering EOF from
the left side of the pipe.

Forward the EOF to GoTTY, and let the server-side decide if it wants to
terminate the connection. The server closes the connection, and in
response we terminate the readLoop which in turn signals writeLoop to
terminate via the QuitChan.

This allows for the user to pipe commands to gotty-client, and capture
all the result sent by the server. For eg. when gotty is launched as
`gotty -w bash`, the following command would now wait to capture all
output from the server.

for (( i = 0 ; i < 2; ++i )); do echo echo $i; echo sleep 2; done | ./gotty-client https://gotty.example.com

Before this patch, gotty-client used to exit on encountering EOF from
the left side of the pipe.
@moul moul self-assigned this Nov 17, 2016
@moul
Copy link
Owner

moul commented Nov 17, 2016

Hi @gurjeet, it looks great, thanks !

LGTM


@QuentinPerez LGTY ?

@QuentinPerez
Copy link
Contributor

QuentinPerez commented Nov 17, 2016

Hi @gurjeet, thank you for your contribution. 😊

Currently your changes don't work on my mac (it stays on wg.Wait()), but I investigated on the issue and you just need to change the line gotty-client.go:231 done := make(chan bool) with done := make(chan bool, 2)

@gurjeet
Copy link
Contributor Author

gurjeet commented Nov 17, 2016

@QuentinPerez What's the error/problem you see with current state of the PR?

I can make that change, but would like to know why making the channel buffered is necessary? From what I can tell, the code should work as expected even with unbuffered channel.

FWIW, it works for me and I am using Go version 1.6.3 on MacOS 10.11.6 (El Capitan).

@QuentinPerez
Copy link
Contributor

QuentinPerez commented Nov 18, 2016

The problem comes from the done channel, there are two fonctions which try to send true.
With an unbuffered channel, the goroutine will not block and call wg.Done() to release wg.Wait()

my patch to show the problem

diff --git a/gotty-client.go b/gotty-client.go
index f376a57..57b25fe 100644
--- a/gotty-client.go
+++ b/gotty-client.go
@@ -243,6 +243,7 @@ func (c *Client) Loop() error {
        }
    case <-c.QuitChan:
    }
+   fmt.Println("WAIT")
    wg.Wait()
    return nil
 }
@@ -318,7 +319,9 @@ func (c *Client) writeLoop(done chan bool, wg *sync.WaitGroup) {
                    err = c.write(append([]byte("0"), byte(4)))

                    if err != nil {
+                       fmt.Println("1: try to send true")
                        done <- true
+                       fmt.Println("1: success")
                        return
                    }
                    continue
@@ -362,7 +365,9 @@ func (c *Client) readLoop(done chan bool, wg *sync.WaitGroup) {
            return
        case msg := <-msgChan:
            if msg.Err != nil {
+               fmt.Println("2: try to send true")
                done <- true
+               fmt.Println("2: success")
                if _, ok := msg.Err.(*websocket.CloseError); !ok {
                    logrus.Warnf("c.Conn.ReadMessage: %v", msg.Err)
                }
@@ -375,12 +380,12 @@ func (c *Client) readLoop(done chan bool, wg *sync.WaitGroup) {
            }
            switch msg.Data[0] {
            case '0': // data
-               buf, err := base64.StdEncoding.DecodeString(string(msg.Data[1:]))
+               _, err := base64.StdEncoding.DecodeString(string(msg.Data[1:]))
                if err != nil {
                    logrus.Warnf("Invalid base64 content: %q", msg.Data[1:])
                    break
                }
-               c.Output.Write(buf)
+               // c.Output.Write(buf)
            case '1': // pong
            case '2': // new title
                newTitle := string(msg.Data[1:])

and the output

$ for (( i = 0 ; i < 2; ++i )); do echo echo $i; echo sleep 2; done | ./gotty-client http://127.0.0.1:8080/
2: try to send true
2: success
WAIT
1: try to send true
# <- can not send true here

@moul
Copy link
Owner

moul commented Nov 28, 2016

Thanks @QuentinPerez!
@gurjeet, are you OK with his investigation ?

@davidfetter
Copy link
Contributor

Per Gurjeet, this seems somewhat fragile. How about setting up a channel that listens for a "poison pill" in the goroutines, i.e. when one dies, they all do. This would be more stable against the addition of additional goroutines to the project, as this proposal is not.

func die(fname string, poison chan bool) PosionAction {
logrus.Info(fname + " died")

was_open := <-poison

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't use underscores in Go names; var was_open should be wasOpen

Killed
)

func open_poison(fname string, poison chan bool) PosionAction {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't use underscores in Go names; func open_poison should be openPoison

type PosionAction int

const (
CommittedSuicide = iota

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exported const CommittedSuicide should have comment (or a comment on this block) or be unexported

@@ -255,8 +250,47 @@ type winsize struct {
y uint16
}

func (c *Client) termsizeLoop(wg *sync.WaitGroup) {
type PosionAction int

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exported type PosionAction should have comment or be unexported

@gurjeet
Copy link
Contributor Author

gurjeet commented Dec 17, 2016

Thanks @davidfetter!

@QuentinPerez @moul, I have pushed another commit into this PR. This specifically addresses the fragility of the current method of killing spawned-goroutnes.

We now use a "poison-pill" in these goroutines, enabling the exit of any one of them to kill the rest of them cleanly.

Can you please try to poke holes in this PR.

All cooperating goroutines regularly try to read from the shared "poison"
channel.  If the read succeeds, they exit by calling die(), assuming
somebody else cracked open the poison pill.

When any of these goroutines is done with its job, it signals other
goroutines to exit by calling open_poison() on the shared channel.

This approach takes advantage of the fact that reads from a closed
channel always succeed.

The driving goroutine (Client.Loop() in this case), is called from the
"main" goroutine. And because when the "main" goroutine exits, the whole
program exit (using os.Exit()) irrepective of liveness of other goroutines,
we could not use the same "poison" channel to wait in the driving goroutine.

Instead, we use sync.WaitGroup to wait for spawned goroutines, because
we want the spawned goroutines to cleanup and exit cleanly.
@davidfetter
Copy link
Contributor

What's the latest on this?

@QuentinPerez
Copy link
Contributor

LGTM thank you @gurjeet 😃

@moul moul merged commit ce15e25 into moul:master Jan 5, 2017
@moul moul removed the review label Jan 5, 2017
@davidfetter
Copy link
Contributor

Any chance of a 1.6.1 release with this change included?

@moul
Copy link
Owner

moul commented Jan 19, 2017

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants