Skip to content

Commit 900d043

Browse files
committed
Introduce ReadPasswordWithContext
Introduced the ReadPasswordWithContext function that allows to read a password from the terminal with support for cancellation. Signed-off-by: grisu48 <grisu42@gmail.com>
1 parent d974fe8 commit 900d043

File tree

4 files changed

+120
-0
lines changed

4 files changed

+120
-0
lines changed

term.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
// Note that on non-Unix systems os.Stdin.Fd() may not be 0.
1717
package term
1818

19+
import "context"
20+
1921
// State contains the state of a terminal.
2022
type State struct {
2123
state
@@ -58,3 +60,10 @@ func GetSize(fd int) (width, height int, err error) {
5860
func ReadPassword(fd int) ([]byte, error) {
5961
return readPassword(fd)
6062
}
63+
64+
// ReadPasswordWithContext reads a line of input from a terminal without local
65+
// echo. This call is similar to ReadPassword but allows the input to be cancelled
66+
// via the provided Context.
67+
func ReadPasswordWithContext(fd int, ctx context.Context) ([]byte, error) {
68+
return readPasswordWithContext(fd, ctx)
69+
}

term_unix.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
package term
99

1010
import (
11+
"context"
12+
"syscall"
13+
"time"
14+
1115
"golang.org/x/sys/unix"
1216
)
1317

@@ -90,3 +94,64 @@ func readPassword(fd int) ([]byte, error) {
9094

9195
return readPasswordLine(passwordReader(fd))
9296
}
97+
98+
func readPasswordWithContext(fd int, ctx context.Context) ([]byte, error) {
99+
termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
100+
nonblocking := false
101+
if err != nil {
102+
return nil, err
103+
}
104+
newState := *termios
105+
newState.Lflag &^= unix.ECHO
106+
newState.Lflag |= unix.ICANON | unix.ISIG
107+
newState.Iflag |= unix.ICRNL
108+
109+
if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, &newState); err != nil {
110+
return nil, err
111+
}
112+
defer func() {
113+
if nonblocking {
114+
unix.SetNonblock(fd, false)
115+
}
116+
unix.IoctlSetTermios(fd, ioctlWriteTermios, termios)
117+
}()
118+
119+
// Set nonblocking IO
120+
if err := unix.SetNonblock(fd, true); err != nil {
121+
return nil, err
122+
}
123+
nonblocking = true
124+
125+
var ret []byte
126+
var buf [1]byte
127+
for {
128+
if ctx.Err() != nil {
129+
return ret, ctx.Err()
130+
}
131+
n, err := unix.Read(fd, buf[:])
132+
if err != nil {
133+
// Check for nonblocking error
134+
if serr, ok := err.(syscall.Errno); ok {
135+
if serr == 11 {
136+
// Add (hopefully not noticable) latency to prevent CPU hogging
137+
time.Sleep(50 * time.Millisecond)
138+
continue
139+
}
140+
}
141+
return ret, err
142+
}
143+
if n > 0 {
144+
switch buf[0] {
145+
case '\b':
146+
if len(ret) > 0 {
147+
ret = ret[:len(ret)-1]
148+
}
149+
case '\n':
150+
return ret, nil
151+
default:
152+
ret = append(ret, buf[0])
153+
}
154+
continue
155+
}
156+
}
157+
}

term_windows.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package term
66

77
import (
8+
"context"
89
"os"
910

1011
"golang.org/x/sys/windows"
@@ -77,3 +78,47 @@ func readPassword(fd int) ([]byte, error) {
7778
defer f.Close()
7879
return readPasswordLine(f)
7980
}
81+
82+
func readPasswordWithContext(fd int, ctx context.Context) ([]byte, error) {
83+
var st uint32
84+
if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil {
85+
return nil, err
86+
}
87+
old := st
88+
89+
st &^= (windows.ENABLE_ECHO_INPUT | windows.ENABLE_LINE_INPUT)
90+
st |= (windows.ENABLE_PROCESSED_OUTPUT | windows.ENABLE_PROCESSED_INPUT)
91+
if err := windows.SetConsoleMode(windows.Handle(fd), st); err != nil {
92+
return nil, err
93+
}
94+
95+
defer windows.SetConsoleMode(windows.Handle(fd), old)
96+
97+
var h windows.Handle
98+
p, _ := windows.GetCurrentProcess()
99+
if err := windows.DuplicateHandle(p, windows.Handle(fd), p, &h, 0, false, windows.DUPLICATE_SAME_ACCESS); err != nil {
100+
return nil, err
101+
}
102+
103+
f := os.NewFile(uintptr(h), "stdin")
104+
defer f.Close()
105+
106+
// Buffer for reading in separate goroutine
107+
type Buffer struct {
108+
line []byte
109+
err error
110+
}
111+
doneChannel := make(chan Buffer, 1)
112+
go func() {
113+
// The following blocks and cannot be unblocked
114+
ret, err := readPasswordLine(f)
115+
doneChannel <- Buffer{line: ret, err: err}
116+
}()
117+
select {
118+
case <-ctx.Done():
119+
f.Close() // Blocks until terminal receives a return key :-(
120+
return make([]byte, 0), ctx.Err()
121+
case buf := <-doneChannel:
122+
return buf.line, buf.err
123+
}
124+
}

terminal_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package term
66

77
import (
88
"bytes"
9+
"context"
910
"io"
1011
"os"
1112
"runtime"

0 commit comments

Comments
 (0)