├── go.mod ├── go.sum ├── .golangci.yml ├── pty_unix.go ├── tc_zos.go ├── pty_zos.go ├── README.md ├── pty_freebsd_nocgo.go ├── console_other.go ├── pty_freebsd_cgo.go ├── .github └── workflows │ └── ci.yml ├── tc_darwin.go ├── tc_netbsd.go ├── tc_openbsd_nocgo.go ├── tc_openbsd_cgo.go ├── tc_freebsd_cgo.go ├── tc_linux.go ├── tc_freebsd_nocgo.go ├── console_linux_test.go ├── console.go ├── tc_unix.go ├── console_test.go ├── console_unix.go ├── console_windows.go ├── console_linux.go └── LICENSE /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/containerd/console 2 | 3 | go 1.13 4 | 5 | require golang.org/x/sys v0.1.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= 2 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 3 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - gofmt 4 | - goimports 5 | - ineffassign 6 | - misspell 7 | - revive 8 | - staticcheck 9 | - structcheck 10 | - unconvert 11 | - unused 12 | - varcheck 13 | - vet 14 | disable: 15 | - errcheck 16 | 17 | run: 18 | timeout: 3m 19 | skip-dirs: 20 | - vendor 21 | -------------------------------------------------------------------------------- /pty_unix.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || linux || netbsd || openbsd 2 | // +build darwin linux netbsd openbsd 3 | 4 | /* 5 | Copyright The containerd Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package console 21 | 22 | import ( 23 | "os" 24 | 25 | "golang.org/x/sys/unix" 26 | ) 27 | 28 | // openpt allocates a new pseudo-terminal by opening the /dev/ptmx device 29 | func openpt() (*os.File, error) { 30 | return os.OpenFile("/dev/ptmx", unix.O_RDWR|unix.O_NOCTTY|unix.O_CLOEXEC, 0) 31 | } 32 | -------------------------------------------------------------------------------- /tc_zos.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package console 18 | 19 | import ( 20 | "strings" 21 | 22 | "golang.org/x/sys/unix" 23 | ) 24 | 25 | const ( 26 | cmdTcGet = unix.TCGETS 27 | cmdTcSet = unix.TCSETS 28 | ) 29 | 30 | // unlockpt is a no-op on zos. 31 | func unlockpt(File) error { 32 | return nil 33 | } 34 | 35 | // ptsname retrieves the name of the first available pts for the given master. 36 | func ptsname(f File) (string, error) { 37 | return "/dev/ttyp" + strings.TrimPrefix(f.Name(), "/dev/ptyp"), nil 38 | } 39 | -------------------------------------------------------------------------------- /pty_zos.go: -------------------------------------------------------------------------------- 1 | //go:build zos 2 | // +build zos 3 | 4 | /* 5 | Copyright The containerd Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package console 21 | 22 | import ( 23 | "fmt" 24 | "os" 25 | ) 26 | 27 | // openpt allocates a new pseudo-terminal by opening the first available /dev/ptypXX device 28 | func openpt() (*os.File, error) { 29 | var f *os.File 30 | var err error 31 | for i := 0; ; i++ { 32 | ptyp := fmt.Sprintf("/dev/ptyp%04d", i) 33 | f, err = os.OpenFile(ptyp, os.O_RDWR, 0600) 34 | if err == nil { 35 | break 36 | } 37 | if os.IsNotExist(err) { 38 | return nil, err 39 | } 40 | // else probably Resource Busy 41 | } 42 | return f, nil 43 | } 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # console 2 | 3 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/containerd/console)](https://pkg.go.dev/github.com/containerd/console) 4 | [![Build Status](https://github.com/containerd/console/workflows/CI/badge.svg)](https://github.com/containerd/console/actions?query=workflow%3ACI) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/containerd/console)](https://goreportcard.com/report/github.com/containerd/console) 6 | 7 | Golang package for dealing with consoles. Light on deps and a simple API. 8 | 9 | ## Modifying the current process 10 | 11 | ```go 12 | current := console.Current() 13 | defer current.Reset() 14 | 15 | if err := current.SetRaw(); err != nil { 16 | } 17 | ws, err := current.Size() 18 | current.Resize(ws) 19 | ``` 20 | 21 | ## Project details 22 | 23 | console is a containerd sub-project, licensed under the [Apache 2.0 license](./LICENSE). 24 | As a containerd sub-project, you will find the: 25 | * [Project governance](https://github.com/containerd/project/blob/main/GOVERNANCE.md), 26 | * [Maintainers](https://github.com/containerd/project/blob/main/MAINTAINERS), 27 | * and [Contributing guidelines](https://github.com/containerd/project/blob/main/CONTRIBUTING.md) 28 | 29 | information in our [`containerd/project`](https://github.com/containerd/project) repository. 30 | -------------------------------------------------------------------------------- /pty_freebsd_nocgo.go: -------------------------------------------------------------------------------- 1 | //go:build freebsd && !cgo 2 | // +build freebsd,!cgo 3 | 4 | /* 5 | Copyright The containerd Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package console 21 | 22 | import ( 23 | "os" 24 | ) 25 | 26 | // 27 | // Implementing the functions below requires cgo support. Non-cgo stubs 28 | // versions are defined below to enable cross-compilation of source code 29 | // that depends on these functions, but the resultant cross-compiled 30 | // binaries cannot actually be used. If the stub function(s) below are 31 | // actually invoked they will display an error message and cause the 32 | // calling process to exit. 33 | // 34 | 35 | func openpt() (*os.File, error) { 36 | panic("openpt() support requires cgo.") 37 | } 38 | -------------------------------------------------------------------------------- /console_other.go: -------------------------------------------------------------------------------- 1 | //go:build !darwin && !freebsd && !linux && !netbsd && !openbsd && !windows && !zos 2 | // +build !darwin,!freebsd,!linux,!netbsd,!openbsd,!windows,!zos 3 | 4 | /* 5 | Copyright The containerd Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package console 21 | 22 | // NewPty creates a new pty pair 23 | // The master is returned as the first console and a string 24 | // with the path to the pty slave is returned as the second 25 | func NewPty() (Console, string, error) { 26 | return nil, "", ErrNotImplemented 27 | } 28 | 29 | // checkConsole checks if the provided file is a console 30 | func checkConsole(f File) error { 31 | return ErrNotAConsole 32 | } 33 | 34 | func newMaster(f File) (Console, error) { 35 | return nil, ErrNotImplemented 36 | } 37 | -------------------------------------------------------------------------------- /pty_freebsd_cgo.go: -------------------------------------------------------------------------------- 1 | //go:build freebsd && cgo 2 | // +build freebsd,cgo 3 | 4 | /* 5 | Copyright The containerd Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package console 21 | 22 | import ( 23 | "fmt" 24 | "os" 25 | ) 26 | 27 | /* 28 | #include 29 | #include 30 | #include 31 | */ 32 | import "C" 33 | 34 | // openpt allocates a new pseudo-terminal and establishes a connection with its 35 | // control device. 36 | func openpt() (*os.File, error) { 37 | fd, err := C.posix_openpt(C.O_RDWR) 38 | if err != nil { 39 | return nil, fmt.Errorf("posix_openpt: %w", err) 40 | } 41 | if _, err := C.grantpt(fd); err != nil { 42 | C.close(fd) 43 | return nil, fmt.Errorf("grantpt: %w", err) 44 | } 45 | return os.NewFile(uintptr(fd), ""), nil 46 | } 47 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Console CI 13 | runs-on: ubuntu-22.04 14 | timeout-minutes: 5 15 | steps: 16 | 17 | - name: Set up Go 18 | uses: actions/setup-go@v3 19 | with: 20 | go-version: 1.19.x 21 | id: go 22 | 23 | - name: Setup Go binary path 24 | shell: bash 25 | run: | 26 | echo "GOPATH=${{ github.workspace }}" >> $GITHUB_ENV 27 | echo "${{ github.workspace }}/bin" >> $GITHUB_PATH 28 | 29 | - name: Check out code 30 | uses: actions/checkout@v3 31 | with: 32 | path: src/github.com/containerd/console 33 | fetch-depth: 25 34 | 35 | - name: Install dependencies 36 | run: | 37 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.50.1 38 | 39 | - name: Project Checks 40 | uses: containerd/project-checks@v1.2.2 41 | with: 42 | working-directory: src/github.com/containerd/console 43 | 44 | - name: Go Linting 45 | run: GOGC=75 golangci-lint run 46 | working-directory: src/github.com/containerd/console 47 | 48 | - name: Build & Test 49 | working-directory: src/github.com/containerd/console 50 | run: | 51 | go test -race 52 | GOOS=openbsd go build 53 | GOOS=windows go build 54 | -------------------------------------------------------------------------------- /tc_darwin.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package console 18 | 19 | import ( 20 | "fmt" 21 | 22 | "golang.org/x/sys/unix" 23 | ) 24 | 25 | const ( 26 | cmdTcGet = unix.TIOCGETA 27 | cmdTcSet = unix.TIOCSETA 28 | ) 29 | 30 | // unlockpt unlocks the slave pseudoterminal device corresponding to the master pseudoterminal referred to by f. 31 | // unlockpt should be called before opening the slave side of a pty. 32 | func unlockpt(f File) error { 33 | return unix.IoctlSetPointerInt(int(f.Fd()), unix.TIOCPTYUNLK, 0) 34 | } 35 | 36 | // ptsname retrieves the name of the first available pts for the given master. 37 | func ptsname(f File) (string, error) { 38 | n, err := unix.IoctlGetInt(int(f.Fd()), unix.TIOCPTYGNAME) 39 | if err != nil { 40 | return "", err 41 | } 42 | return fmt.Sprintf("/dev/pts/%d", n), nil 43 | } 44 | -------------------------------------------------------------------------------- /tc_netbsd.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package console 18 | 19 | import ( 20 | "bytes" 21 | 22 | "golang.org/x/sys/unix" 23 | ) 24 | 25 | const ( 26 | cmdTcGet = unix.TIOCGETA 27 | cmdTcSet = unix.TIOCSETA 28 | ) 29 | 30 | // unlockpt unlocks the slave pseudoterminal device corresponding to the master pseudoterminal referred to by f. 31 | // unlockpt should be called before opening the slave side of a pty. 32 | // This does not exist on NetBSD, it does not allocate controlling terminals on open 33 | func unlockpt(f File) error { 34 | return nil 35 | } 36 | 37 | // ptsname retrieves the name of the first available pts for the given master. 38 | func ptsname(f File) (string, error) { 39 | ptm, err := unix.IoctlGetPtmget(int(f.Fd()), unix.TIOCPTSNAME) 40 | if err != nil { 41 | return "", err 42 | } 43 | return string(ptm.Sn[:bytes.IndexByte(ptm.Sn[:], 0)]), nil 44 | } 45 | -------------------------------------------------------------------------------- /tc_openbsd_nocgo.go: -------------------------------------------------------------------------------- 1 | //go:build openbsd && !cgo 2 | // +build openbsd,!cgo 3 | 4 | /* 5 | Copyright The containerd Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | // 21 | // Implementing the functions below requires cgo support. Non-cgo stubs 22 | // versions are defined below to enable cross-compilation of source code 23 | // that depends on these functions, but the resultant cross-compiled 24 | // binaries cannot actually be used. If the stub function(s) below are 25 | // actually invoked they will display an error message and cause the 26 | // calling process to exit. 27 | // 28 | 29 | package console 30 | 31 | import ( 32 | "golang.org/x/sys/unix" 33 | ) 34 | 35 | const ( 36 | cmdTcGet = unix.TIOCGETA 37 | cmdTcSet = unix.TIOCSETA 38 | ) 39 | 40 | func ptsname(f File) (string, error) { 41 | panic("ptsname() support requires cgo.") 42 | } 43 | 44 | func unlockpt(f File) error { 45 | panic("unlockpt() support requires cgo.") 46 | } 47 | -------------------------------------------------------------------------------- /tc_openbsd_cgo.go: -------------------------------------------------------------------------------- 1 | //go:build openbsd && cgo 2 | // +build openbsd,cgo 3 | 4 | /* 5 | Copyright The containerd Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package console 21 | 22 | import ( 23 | "golang.org/x/sys/unix" 24 | ) 25 | 26 | //#include 27 | import "C" 28 | 29 | const ( 30 | cmdTcGet = unix.TIOCGETA 31 | cmdTcSet = unix.TIOCSETA 32 | ) 33 | 34 | // ptsname retrieves the name of the first available pts for the given master. 35 | func ptsname(f File) (string, error) { 36 | ptspath, err := C.ptsname(C.int(f.Fd())) 37 | if err != nil { 38 | return "", err 39 | } 40 | return C.GoString(ptspath), nil 41 | } 42 | 43 | // unlockpt unlocks the slave pseudoterminal device corresponding to the master pseudoterminal referred to by f. 44 | // unlockpt should be called before opening the slave side of a pty. 45 | func unlockpt(f File) error { 46 | if _, err := C.grantpt(C.int(f.Fd())); err != nil { 47 | return err 48 | } 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /tc_freebsd_cgo.go: -------------------------------------------------------------------------------- 1 | //go:build freebsd && cgo 2 | // +build freebsd,cgo 3 | 4 | /* 5 | Copyright The containerd Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package console 21 | 22 | import ( 23 | "fmt" 24 | 25 | "golang.org/x/sys/unix" 26 | ) 27 | 28 | /* 29 | #include 30 | #include 31 | */ 32 | import "C" 33 | 34 | const ( 35 | cmdTcGet = unix.TIOCGETA 36 | cmdTcSet = unix.TIOCSETA 37 | ) 38 | 39 | // unlockpt unlocks the slave pseudoterminal device corresponding to the master pseudoterminal referred to by f. 40 | // unlockpt should be called before opening the slave side of a pty. 41 | func unlockpt(f File) error { 42 | fd := C.int(f.Fd()) 43 | if _, err := C.unlockpt(fd); err != nil { 44 | C.close(fd) 45 | return fmt.Errorf("unlockpt: %w", err) 46 | } 47 | return nil 48 | } 49 | 50 | // ptsname retrieves the name of the first available pts for the given master. 51 | func ptsname(f File) (string, error) { 52 | n, err := unix.IoctlGetInt(int(f.Fd()), unix.TIOCGPTN) 53 | if err != nil { 54 | return "", err 55 | } 56 | return fmt.Sprintf("/dev/pts/%d", n), nil 57 | } 58 | -------------------------------------------------------------------------------- /tc_linux.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package console 18 | 19 | import ( 20 | "fmt" 21 | "unsafe" 22 | 23 | "golang.org/x/sys/unix" 24 | ) 25 | 26 | const ( 27 | cmdTcGet = unix.TCGETS 28 | cmdTcSet = unix.TCSETS 29 | ) 30 | 31 | // unlockpt unlocks the slave pseudoterminal device corresponding to the master pseudoterminal referred to by f. 32 | // unlockpt should be called before opening the slave side of a pty. 33 | func unlockpt(f File) error { 34 | var u int32 35 | // XXX do not use unix.IoctlSetPointerInt here, see commit dbd69c59b81. 36 | if _, _, err := unix.Syscall(unix.SYS_IOCTL, f.Fd(), unix.TIOCSPTLCK, uintptr(unsafe.Pointer(&u))); err != 0 { 37 | return err 38 | } 39 | return nil 40 | } 41 | 42 | // ptsname retrieves the name of the first available pts for the given master. 43 | func ptsname(f File) (string, error) { 44 | var u uint32 45 | // XXX do not use unix.IoctlGetInt here, see commit dbd69c59b81. 46 | if _, _, err := unix.Syscall(unix.SYS_IOCTL, f.Fd(), unix.TIOCGPTN, uintptr(unsafe.Pointer(&u))); err != 0 { 47 | return "", err 48 | } 49 | return fmt.Sprintf("/dev/pts/%d", u), nil 50 | } 51 | -------------------------------------------------------------------------------- /tc_freebsd_nocgo.go: -------------------------------------------------------------------------------- 1 | //go:build freebsd && !cgo 2 | // +build freebsd,!cgo 3 | 4 | /* 5 | Copyright The containerd Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package console 21 | 22 | import ( 23 | "fmt" 24 | 25 | "golang.org/x/sys/unix" 26 | ) 27 | 28 | const ( 29 | cmdTcGet = unix.TIOCGETA 30 | cmdTcSet = unix.TIOCSETA 31 | ) 32 | 33 | // 34 | // Implementing the functions below requires cgo support. Non-cgo stubs 35 | // versions are defined below to enable cross-compilation of source code 36 | // that depends on these functions, but the resultant cross-compiled 37 | // binaries cannot actually be used. If the stub function(s) below are 38 | // actually invoked they will display an error message and cause the 39 | // calling process to exit. 40 | // 41 | 42 | // unlockpt unlocks the slave pseudoterminal device corresponding to the master pseudoterminal referred to by f. 43 | // unlockpt should be called before opening the slave side of a pty. 44 | func unlockpt(f File) error { 45 | panic("unlockpt() support requires cgo.") 46 | } 47 | 48 | // ptsname retrieves the name of the first available pts for the given master. 49 | func ptsname(f File) (string, error) { 50 | n, err := unix.IoctlGetInt(int(f.Fd()), unix.TIOCGPTN) 51 | if err != nil { 52 | return "", err 53 | } 54 | return fmt.Sprintf("/dev/pts/%d", n), nil 55 | } 56 | -------------------------------------------------------------------------------- /console_linux_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package console 18 | 19 | import ( 20 | "bytes" 21 | "fmt" 22 | "io" 23 | "os" 24 | "os/exec" 25 | "sync" 26 | "testing" 27 | ) 28 | 29 | func TestEpollConsole(t *testing.T) { 30 | console, slavePath, err := NewPty() 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | defer console.Close() 35 | 36 | slave, err := os.OpenFile(slavePath, os.O_RDWR, 0) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | defer slave.Close() 41 | 42 | iteration := 10 43 | 44 | cmd := exec.Command("sh", "-c", fmt.Sprintf("for x in `seq 1 %d`; do echo -n test; done", iteration)) 45 | cmd.Stdin = slave 46 | cmd.Stdout = slave 47 | cmd.Stderr = slave 48 | 49 | epoller, err := NewEpoller() 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | epollConsole, err := epoller.Add(console) 54 | if err != nil { 55 | t.Fatal(err) 56 | } 57 | go epoller.Wait() 58 | 59 | var ( 60 | b bytes.Buffer 61 | wg sync.WaitGroup 62 | ) 63 | wg.Add(1) 64 | go func() { 65 | io.Copy(&b, epollConsole) 66 | wg.Done() 67 | }() 68 | 69 | if err := cmd.Run(); err != nil { 70 | t.Fatal(err) 71 | } 72 | slave.Close() 73 | if err := epollConsole.Shutdown(epoller.CloseConsole); err != nil { 74 | t.Fatal(err) 75 | } 76 | wg.Wait() 77 | if err := epollConsole.Close(); err != nil { 78 | t.Fatal(err) 79 | } 80 | 81 | expectedOutput := "" 82 | for i := 0; i < iteration; i++ { 83 | expectedOutput += "test" 84 | } 85 | if out := b.String(); out != expectedOutput { 86 | t.Errorf("unexpected output %q", out) 87 | } 88 | 89 | // make sure multiple Close calls return os.ErrClosed after the first 90 | if err := epoller.Close(); err != nil { 91 | t.Fatal(err) 92 | } 93 | if err := epoller.Close(); err != os.ErrClosed { 94 | t.Fatalf("unexpected error returned from second call to epoller.Close(): %v", err) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /console.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package console 18 | 19 | import ( 20 | "errors" 21 | "io" 22 | "os" 23 | ) 24 | 25 | var ( 26 | ErrNotAConsole = errors.New("provided file is not a console") 27 | ErrNotImplemented = errors.New("not implemented") 28 | ) 29 | 30 | type File interface { 31 | io.ReadWriteCloser 32 | 33 | // Fd returns its file descriptor 34 | Fd() uintptr 35 | // Name returns its file name 36 | Name() string 37 | } 38 | 39 | type Console interface { 40 | File 41 | 42 | // Resize resizes the console to the provided window size 43 | Resize(WinSize) error 44 | // ResizeFrom resizes the calling console to the size of the 45 | // provided console 46 | ResizeFrom(Console) error 47 | // SetRaw sets the console in raw mode 48 | SetRaw() error 49 | // DisableEcho disables echo on the console 50 | DisableEcho() error 51 | // Reset restores the console to its original state 52 | Reset() error 53 | // Size returns the window size of the console 54 | Size() (WinSize, error) 55 | } 56 | 57 | // WinSize specifies the window size of the console 58 | type WinSize struct { 59 | // Height of the console 60 | Height uint16 61 | // Width of the console 62 | Width uint16 63 | x uint16 64 | y uint16 65 | } 66 | 67 | // Current returns the current process' console 68 | func Current() (c Console) { 69 | var err error 70 | // Usually all three streams (stdin, stdout, and stderr) 71 | // are open to the same console, but some might be redirected, 72 | // so try all three. 73 | for _, s := range []*os.File{os.Stderr, os.Stdout, os.Stdin} { 74 | if c, err = ConsoleFromFile(s); err == nil { 75 | return c 76 | } 77 | } 78 | // One of the std streams should always be a console 79 | // for the design of this function. 80 | panic(err) 81 | } 82 | 83 | // ConsoleFromFile returns a console using the provided file 84 | // nolint:revive 85 | func ConsoleFromFile(f File) (Console, error) { 86 | if err := checkConsole(f); err != nil { 87 | return nil, err 88 | } 89 | return newMaster(f) 90 | } 91 | -------------------------------------------------------------------------------- /tc_unix.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || freebsd || linux || netbsd || openbsd || zos 2 | // +build darwin freebsd linux netbsd openbsd zos 3 | 4 | /* 5 | Copyright The containerd Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package console 21 | 22 | import ( 23 | "golang.org/x/sys/unix" 24 | ) 25 | 26 | func tcget(fd uintptr, p *unix.Termios) error { 27 | termios, err := unix.IoctlGetTermios(int(fd), cmdTcGet) 28 | if err != nil { 29 | return err 30 | } 31 | *p = *termios 32 | return nil 33 | } 34 | 35 | func tcset(fd uintptr, p *unix.Termios) error { 36 | return unix.IoctlSetTermios(int(fd), cmdTcSet, p) 37 | } 38 | 39 | func tcgwinsz(fd uintptr) (WinSize, error) { 40 | var ws WinSize 41 | 42 | uws, err := unix.IoctlGetWinsize(int(fd), unix.TIOCGWINSZ) 43 | if err != nil { 44 | return ws, err 45 | } 46 | 47 | // Translate from unix.Winsize to console.WinSize 48 | ws.Height = uws.Row 49 | ws.Width = uws.Col 50 | ws.x = uws.Xpixel 51 | ws.y = uws.Ypixel 52 | return ws, nil 53 | } 54 | 55 | func tcswinsz(fd uintptr, ws WinSize) error { 56 | // Translate from console.WinSize to unix.Winsize 57 | 58 | var uws unix.Winsize 59 | uws.Row = ws.Height 60 | uws.Col = ws.Width 61 | uws.Xpixel = ws.x 62 | uws.Ypixel = ws.y 63 | 64 | return unix.IoctlSetWinsize(int(fd), unix.TIOCSWINSZ, &uws) 65 | } 66 | 67 | func setONLCR(fd uintptr, enable bool) error { 68 | var termios unix.Termios 69 | if err := tcget(fd, &termios); err != nil { 70 | return err 71 | } 72 | if enable { 73 | // Set +onlcr so we can act like a real terminal 74 | termios.Oflag |= unix.ONLCR 75 | } else { 76 | // Set -onlcr so we don't have to deal with \r. 77 | termios.Oflag &^= unix.ONLCR 78 | } 79 | return tcset(fd, &termios) 80 | } 81 | 82 | func cfmakeraw(t unix.Termios) unix.Termios { 83 | t.Iflag &^= (unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON) 84 | t.Oflag &^= unix.OPOST 85 | t.Lflag &^= (unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN) 86 | t.Cflag &^= (unix.CSIZE | unix.PARENB) 87 | t.Cflag |= unix.CS8 88 | t.Cc[unix.VMIN] = 1 89 | t.Cc[unix.VTIME] = 0 90 | 91 | return t 92 | } 93 | -------------------------------------------------------------------------------- /console_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux || zos || freebsd 2 | // +build linux zos freebsd 3 | 4 | /* 5 | Copyright The containerd Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package console 21 | 22 | import ( 23 | "bytes" 24 | "io" 25 | "os" 26 | "os/exec" 27 | "sync" 28 | "testing" 29 | ) 30 | 31 | func TestWinSize(t *testing.T) { 32 | c, _, err := NewPty() 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | defer c.Close() 37 | if err := c.Resize(WinSize{ 38 | Width: 11, 39 | Height: 10, 40 | }); err != nil { 41 | t.Error(err) 42 | return 43 | } 44 | size, err := c.Size() 45 | if err != nil { 46 | t.Error(err) 47 | return 48 | } 49 | if size.Width != 11 { 50 | t.Errorf("width should be 11 but received %d", size.Width) 51 | } 52 | if size.Height != 10 { 53 | t.Errorf("height should be 10 but received %d", size.Height) 54 | } 55 | } 56 | 57 | func testConsolePty(t *testing.T, newPty func() (Console, string, error)) { 58 | console, slavePath, err := newPty() 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | defer console.Close() 63 | 64 | slave, err := os.OpenFile(slavePath, os.O_RDWR, 0) 65 | if err != nil { 66 | t.Fatal(err) 67 | } 68 | defer slave.Close() 69 | 70 | iteration := 10 71 | 72 | var ( 73 | b bytes.Buffer 74 | wg sync.WaitGroup 75 | ) 76 | wg.Add(1) 77 | go func() { 78 | io.Copy(&b, console) 79 | wg.Done() 80 | }() 81 | 82 | for i := 0; i < iteration; i++ { 83 | cmd := exec.Command("sh", "-c", "printf test") 84 | cmd.Stdin = slave 85 | cmd.Stdout = slave 86 | cmd.Stderr = slave 87 | 88 | if err := cmd.Run(); err != nil { 89 | t.Fatal(err) 90 | } 91 | } 92 | slave.Close() 93 | wg.Wait() 94 | 95 | expectedOutput := "" 96 | for i := 0; i < iteration; i++ { 97 | expectedOutput += "test" 98 | } 99 | if out := b.String(); out != expectedOutput { 100 | t.Errorf("unexpected output %q", out) 101 | } 102 | } 103 | 104 | func TestConsolePty_NewPty(t *testing.T) { 105 | testConsolePty(t, NewPty) 106 | } 107 | 108 | func TestConsolePty_NewPtyFromFile(t *testing.T) { 109 | testConsolePty(t, func() (Console, string, error) { 110 | // Equivalent to NewPty(). 111 | f, err := openpt() 112 | if err != nil { 113 | return nil, "", err 114 | } 115 | return NewPtyFromFile(f) 116 | }) 117 | } 118 | -------------------------------------------------------------------------------- /console_unix.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || freebsd || linux || netbsd || openbsd || zos 2 | // +build darwin freebsd linux netbsd openbsd zos 3 | 4 | /* 5 | Copyright The containerd Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package console 21 | 22 | import ( 23 | "golang.org/x/sys/unix" 24 | ) 25 | 26 | // NewPty creates a new pty pair 27 | // The master is returned as the first console and a string 28 | // with the path to the pty slave is returned as the second 29 | func NewPty() (Console, string, error) { 30 | f, err := openpt() 31 | if err != nil { 32 | return nil, "", err 33 | } 34 | return NewPtyFromFile(f) 35 | } 36 | 37 | // NewPtyFromFile creates a new pty pair, just like [NewPty] except that the 38 | // provided [os.File] is used as the master rather than automatically creating 39 | // a new master from /dev/ptmx. The ownership of [os.File] is passed to the 40 | // returned [Console], so the caller must be careful to not call Close on the 41 | // underlying file. 42 | func NewPtyFromFile(f File) (Console, string, error) { 43 | slave, err := ptsname(f) 44 | if err != nil { 45 | return nil, "", err 46 | } 47 | if err := unlockpt(f); err != nil { 48 | return nil, "", err 49 | } 50 | m, err := newMaster(f) 51 | if err != nil { 52 | return nil, "", err 53 | } 54 | return m, slave, nil 55 | } 56 | 57 | type master struct { 58 | f File 59 | original *unix.Termios 60 | } 61 | 62 | func (m *master) Read(b []byte) (int, error) { 63 | return m.f.Read(b) 64 | } 65 | 66 | func (m *master) Write(b []byte) (int, error) { 67 | return m.f.Write(b) 68 | } 69 | 70 | func (m *master) Close() error { 71 | return m.f.Close() 72 | } 73 | 74 | func (m *master) Resize(ws WinSize) error { 75 | return tcswinsz(m.f.Fd(), ws) 76 | } 77 | 78 | func (m *master) ResizeFrom(c Console) error { 79 | ws, err := c.Size() 80 | if err != nil { 81 | return err 82 | } 83 | return m.Resize(ws) 84 | } 85 | 86 | func (m *master) Reset() error { 87 | if m.original == nil { 88 | return nil 89 | } 90 | return tcset(m.f.Fd(), m.original) 91 | } 92 | 93 | func (m *master) getCurrent() (unix.Termios, error) { 94 | var termios unix.Termios 95 | if err := tcget(m.f.Fd(), &termios); err != nil { 96 | return unix.Termios{}, err 97 | } 98 | return termios, nil 99 | } 100 | 101 | func (m *master) SetRaw() error { 102 | rawState, err := m.getCurrent() 103 | if err != nil { 104 | return err 105 | } 106 | rawState = cfmakeraw(rawState) 107 | rawState.Oflag = rawState.Oflag | unix.OPOST 108 | return tcset(m.f.Fd(), &rawState) 109 | } 110 | 111 | func (m *master) DisableEcho() error { 112 | rawState, err := m.getCurrent() 113 | if err != nil { 114 | return err 115 | } 116 | rawState.Lflag = rawState.Lflag &^ unix.ECHO 117 | return tcset(m.f.Fd(), &rawState) 118 | } 119 | 120 | func (m *master) Size() (WinSize, error) { 121 | return tcgwinsz(m.f.Fd()) 122 | } 123 | 124 | func (m *master) Fd() uintptr { 125 | return m.f.Fd() 126 | } 127 | 128 | func (m *master) Name() string { 129 | return m.f.Name() 130 | } 131 | 132 | // checkConsole checks if the provided file is a console 133 | func checkConsole(f File) error { 134 | var termios unix.Termios 135 | if tcget(f.Fd(), &termios) != nil { 136 | return ErrNotAConsole 137 | } 138 | return nil 139 | } 140 | 141 | func newMaster(f File) (Console, error) { 142 | m := &master{ 143 | f: f, 144 | } 145 | t, err := m.getCurrent() 146 | if err != nil { 147 | return nil, err 148 | } 149 | m.original = &t 150 | return m, nil 151 | } 152 | 153 | // ClearONLCR sets the necessary tty_ioctl(4)s to ensure that a pty pair 154 | // created by us acts normally. In particular, a not-very-well-known default of 155 | // Linux unix98 ptys is that they have +onlcr by default. While this isn't a 156 | // problem for terminal emulators, because we relay data from the terminal we 157 | // also relay that funky line discipline. 158 | func ClearONLCR(fd uintptr) error { 159 | return setONLCR(fd, false) 160 | } 161 | 162 | // SetONLCR sets the necessary tty_ioctl(4)s to ensure that a pty pair 163 | // created by us acts as intended for a terminal emulator. 164 | func SetONLCR(fd uintptr) error { 165 | return setONLCR(fd, true) 166 | } 167 | -------------------------------------------------------------------------------- /console_windows.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package console 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "os" 23 | 24 | "golang.org/x/sys/windows" 25 | ) 26 | 27 | var vtInputSupported bool 28 | 29 | func (m *master) initStdios() { 30 | // Note: We discard console mode warnings, because in/out can be redirected. 31 | // 32 | // TODO: Investigate opening CONOUT$/CONIN$ to handle this correctly 33 | 34 | m.in = windows.Handle(os.Stdin.Fd()) 35 | if err := windows.GetConsoleMode(m.in, &m.inMode); err == nil { 36 | // Validate that windows.ENABLE_VIRTUAL_TERMINAL_INPUT is supported, but do not set it. 37 | if err = windows.SetConsoleMode(m.in, m.inMode|windows.ENABLE_VIRTUAL_TERMINAL_INPUT); err == nil { 38 | vtInputSupported = true 39 | } 40 | // Unconditionally set the console mode back even on failure because SetConsoleMode 41 | // remembers invalid bits on input handles. 42 | windows.SetConsoleMode(m.in, m.inMode) 43 | } 44 | 45 | m.out = windows.Handle(os.Stdout.Fd()) 46 | if err := windows.GetConsoleMode(m.out, &m.outMode); err == nil { 47 | if err := windows.SetConsoleMode(m.out, m.outMode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING); err == nil { 48 | m.outMode |= windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING 49 | } else { 50 | windows.SetConsoleMode(m.out, m.outMode) 51 | } 52 | } 53 | 54 | m.err = windows.Handle(os.Stderr.Fd()) 55 | if err := windows.GetConsoleMode(m.err, &m.errMode); err == nil { 56 | if err := windows.SetConsoleMode(m.err, m.errMode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING); err == nil { 57 | m.errMode |= windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING 58 | } else { 59 | windows.SetConsoleMode(m.err, m.errMode) 60 | } 61 | } 62 | } 63 | 64 | type master struct { 65 | in windows.Handle 66 | inMode uint32 67 | 68 | out windows.Handle 69 | outMode uint32 70 | 71 | err windows.Handle 72 | errMode uint32 73 | } 74 | 75 | func (m *master) SetRaw() error { 76 | if err := makeInputRaw(m.in, m.inMode); err != nil { 77 | return err 78 | } 79 | 80 | // Set StdOut and StdErr to raw mode, we ignore failures since 81 | // windows.DISABLE_NEWLINE_AUTO_RETURN might not be supported on this version of 82 | // Windows. 83 | 84 | windows.SetConsoleMode(m.out, m.outMode|windows.DISABLE_NEWLINE_AUTO_RETURN) 85 | 86 | windows.SetConsoleMode(m.err, m.errMode|windows.DISABLE_NEWLINE_AUTO_RETURN) 87 | 88 | return nil 89 | } 90 | 91 | func (m *master) Reset() error { 92 | var errs []error 93 | 94 | for _, s := range []struct { 95 | fd windows.Handle 96 | mode uint32 97 | }{ 98 | {m.in, m.inMode}, 99 | {m.out, m.outMode}, 100 | {m.err, m.errMode}, 101 | } { 102 | if err := windows.SetConsoleMode(s.fd, s.mode); err != nil { 103 | // we can't just abort on the first error, otherwise we might leave 104 | // the console in an unexpected state. 105 | errs = append(errs, fmt.Errorf("unable to restore console mode: %w", err)) 106 | } 107 | } 108 | 109 | if len(errs) > 0 { 110 | return errs[0] 111 | } 112 | 113 | return nil 114 | } 115 | 116 | func (m *master) Size() (WinSize, error) { 117 | var info windows.ConsoleScreenBufferInfo 118 | err := windows.GetConsoleScreenBufferInfo(m.out, &info) 119 | if err != nil { 120 | return WinSize{}, fmt.Errorf("unable to get console info: %w", err) 121 | } 122 | 123 | winsize := WinSize{ 124 | Width: uint16(info.Window.Right - info.Window.Left + 1), 125 | Height: uint16(info.Window.Bottom - info.Window.Top + 1), 126 | } 127 | 128 | return winsize, nil 129 | } 130 | 131 | func (m *master) Resize(ws WinSize) error { 132 | return ErrNotImplemented 133 | } 134 | 135 | func (m *master) ResizeFrom(c Console) error { 136 | return ErrNotImplemented 137 | } 138 | 139 | func (m *master) DisableEcho() error { 140 | mode := m.inMode &^ windows.ENABLE_ECHO_INPUT 141 | mode |= windows.ENABLE_PROCESSED_INPUT 142 | mode |= windows.ENABLE_LINE_INPUT 143 | 144 | if err := windows.SetConsoleMode(m.in, mode); err != nil { 145 | return fmt.Errorf("unable to set console to disable echo: %w", err) 146 | } 147 | 148 | return nil 149 | } 150 | 151 | func (m *master) Close() error { 152 | return nil 153 | } 154 | 155 | func (m *master) Read(b []byte) (int, error) { 156 | return os.Stdin.Read(b) 157 | } 158 | 159 | func (m *master) Write(b []byte) (int, error) { 160 | return os.Stdout.Write(b) 161 | } 162 | 163 | func (m *master) Fd() uintptr { 164 | return uintptr(m.in) 165 | } 166 | 167 | // on windows, console can only be made from os.Std{in,out,err}, hence there 168 | // isnt a single name here we can use. Return a dummy "console" value in this 169 | // case should be sufficient. 170 | func (m *master) Name() string { 171 | return "console" 172 | } 173 | 174 | // makeInputRaw puts the terminal (Windows Console) connected to the given 175 | // file descriptor into raw mode 176 | func makeInputRaw(fd windows.Handle, mode uint32) error { 177 | // See 178 | // -- https://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx 179 | // -- https://msdn.microsoft.com/en-us/library/windows/desktop/ms683462(v=vs.85).aspx 180 | 181 | // Disable these modes 182 | mode &^= windows.ENABLE_ECHO_INPUT 183 | mode &^= windows.ENABLE_LINE_INPUT 184 | mode &^= windows.ENABLE_MOUSE_INPUT 185 | mode &^= windows.ENABLE_WINDOW_INPUT 186 | mode &^= windows.ENABLE_PROCESSED_INPUT 187 | 188 | // Enable these modes 189 | mode |= windows.ENABLE_EXTENDED_FLAGS 190 | mode |= windows.ENABLE_INSERT_MODE 191 | mode |= windows.ENABLE_QUICK_EDIT_MODE 192 | 193 | if vtInputSupported { 194 | mode |= windows.ENABLE_VIRTUAL_TERMINAL_INPUT 195 | } 196 | 197 | if err := windows.SetConsoleMode(fd, mode); err != nil { 198 | return fmt.Errorf("unable to set console to raw mode: %w", err) 199 | } 200 | 201 | return nil 202 | } 203 | 204 | func checkConsole(f File) error { 205 | var mode uint32 206 | if err := windows.GetConsoleMode(windows.Handle(f.Fd()), &mode); err != nil { 207 | return err 208 | } 209 | return nil 210 | } 211 | 212 | func newMaster(f File) (Console, error) { 213 | if f != os.Stdin && f != os.Stdout && f != os.Stderr { 214 | return nil, errors.New("creating a console from a file is not supported on windows") 215 | } 216 | m := &master{} 217 | m.initStdios() 218 | return m, nil 219 | } 220 | -------------------------------------------------------------------------------- /console_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | /* 5 | Copyright The containerd Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package console 21 | 22 | import ( 23 | "io" 24 | "os" 25 | "sync" 26 | 27 | "golang.org/x/sys/unix" 28 | ) 29 | 30 | const ( 31 | maxEvents = 128 32 | ) 33 | 34 | // Epoller manages multiple epoll consoles using edge-triggered epoll api so we 35 | // dont have to deal with repeated wake-up of EPOLLER or EPOLLHUP. 36 | // For more details, see: 37 | // - https://github.com/systemd/systemd/pull/4262 38 | // - https://github.com/moby/moby/issues/27202 39 | // 40 | // Example usage of Epoller and EpollConsole can be as follow: 41 | // 42 | // epoller, _ := NewEpoller() 43 | // epollConsole, _ := epoller.Add(console) 44 | // go epoller.Wait() 45 | // var ( 46 | // b bytes.Buffer 47 | // wg sync.WaitGroup 48 | // ) 49 | // wg.Add(1) 50 | // go func() { 51 | // io.Copy(&b, epollConsole) 52 | // wg.Done() 53 | // }() 54 | // // perform I/O on the console 55 | // epollConsole.Shutdown(epoller.CloseConsole) 56 | // wg.Wait() 57 | // epollConsole.Close() 58 | type Epoller struct { 59 | efd int 60 | mu sync.Mutex 61 | fdMapping map[int]*EpollConsole 62 | closeOnce sync.Once 63 | } 64 | 65 | // NewEpoller returns an instance of epoller with a valid epoll fd. 66 | func NewEpoller() (*Epoller, error) { 67 | efd, err := unix.EpollCreate1(unix.EPOLL_CLOEXEC) 68 | if err != nil { 69 | return nil, err 70 | } 71 | return &Epoller{ 72 | efd: efd, 73 | fdMapping: make(map[int]*EpollConsole), 74 | }, nil 75 | } 76 | 77 | // Add creates an epoll console based on the provided console. The console will 78 | // be registered with EPOLLET (i.e. using edge-triggered notification) and its 79 | // file descriptor will be set to non-blocking mode. After this, user should use 80 | // the return console to perform I/O. 81 | func (e *Epoller) Add(console Console) (*EpollConsole, error) { 82 | sysfd := int(console.Fd()) 83 | // Set sysfd to non-blocking mode 84 | if err := unix.SetNonblock(sysfd, true); err != nil { 85 | return nil, err 86 | } 87 | 88 | ev := unix.EpollEvent{ 89 | Events: unix.EPOLLIN | unix.EPOLLOUT | unix.EPOLLRDHUP | unix.EPOLLET, 90 | Fd: int32(sysfd), 91 | } 92 | if err := unix.EpollCtl(e.efd, unix.EPOLL_CTL_ADD, sysfd, &ev); err != nil { 93 | return nil, err 94 | } 95 | ef := &EpollConsole{ 96 | Console: console, 97 | sysfd: sysfd, 98 | readc: sync.NewCond(&sync.Mutex{}), 99 | writec: sync.NewCond(&sync.Mutex{}), 100 | } 101 | e.mu.Lock() 102 | e.fdMapping[sysfd] = ef 103 | e.mu.Unlock() 104 | return ef, nil 105 | } 106 | 107 | // Wait starts the loop to wait for its consoles' notifications and signal 108 | // appropriate console that it can perform I/O. 109 | func (e *Epoller) Wait() error { 110 | events := make([]unix.EpollEvent, maxEvents) 111 | for { 112 | n, err := unix.EpollWait(e.efd, events, -1) 113 | if err != nil { 114 | // EINTR: The call was interrupted by a signal handler before either 115 | // any of the requested events occurred or the timeout expired 116 | if err == unix.EINTR { 117 | continue 118 | } 119 | return err 120 | } 121 | for i := 0; i < n; i++ { 122 | ev := &events[i] 123 | // the console is ready to be read from 124 | if ev.Events&(unix.EPOLLIN|unix.EPOLLHUP|unix.EPOLLERR) != 0 { 125 | if epfile := e.getConsole(int(ev.Fd)); epfile != nil { 126 | epfile.signalRead() 127 | } 128 | } 129 | // the console is ready to be written to 130 | if ev.Events&(unix.EPOLLOUT|unix.EPOLLHUP|unix.EPOLLERR) != 0 { 131 | if epfile := e.getConsole(int(ev.Fd)); epfile != nil { 132 | epfile.signalWrite() 133 | } 134 | } 135 | } 136 | } 137 | } 138 | 139 | // CloseConsole unregisters the console's file descriptor from epoll interface 140 | func (e *Epoller) CloseConsole(fd int) error { 141 | e.mu.Lock() 142 | defer e.mu.Unlock() 143 | delete(e.fdMapping, fd) 144 | return unix.EpollCtl(e.efd, unix.EPOLL_CTL_DEL, fd, &unix.EpollEvent{}) 145 | } 146 | 147 | func (e *Epoller) getConsole(sysfd int) *EpollConsole { 148 | e.mu.Lock() 149 | f := e.fdMapping[sysfd] 150 | e.mu.Unlock() 151 | return f 152 | } 153 | 154 | // Close closes the epoll fd 155 | func (e *Epoller) Close() error { 156 | closeErr := os.ErrClosed // default to "file already closed" 157 | e.closeOnce.Do(func() { 158 | closeErr = unix.Close(e.efd) 159 | }) 160 | return closeErr 161 | } 162 | 163 | // EpollConsole acts like a console but registers its file descriptor with an 164 | // epoll fd and uses epoll API to perform I/O. 165 | type EpollConsole struct { 166 | Console 167 | readc *sync.Cond 168 | writec *sync.Cond 169 | sysfd int 170 | closed bool 171 | } 172 | 173 | // Read reads up to len(p) bytes into p. It returns the number of bytes read 174 | // (0 <= n <= len(p)) and any error encountered. 175 | // 176 | // If the console's read returns EAGAIN or EIO, we assume that it's a 177 | // temporary error because the other side went away and wait for the signal 178 | // generated by epoll event to continue. 179 | func (ec *EpollConsole) Read(p []byte) (n int, err error) { 180 | var read int 181 | ec.readc.L.Lock() 182 | defer ec.readc.L.Unlock() 183 | for { 184 | read, err = ec.Console.Read(p[n:]) 185 | n += read 186 | if err != nil { 187 | var hangup bool 188 | if perr, ok := err.(*os.PathError); ok { 189 | hangup = (perr.Err == unix.EAGAIN || perr.Err == unix.EIO) 190 | } else { 191 | hangup = (err == unix.EAGAIN || err == unix.EIO) 192 | } 193 | // if the other end disappear, assume this is temporary and wait for the 194 | // signal to continue again. Unless we didnt read anything and the 195 | // console is already marked as closed then we should exit 196 | if hangup && !(n == 0 && len(p) > 0 && ec.closed) { 197 | ec.readc.Wait() 198 | continue 199 | } 200 | } 201 | break 202 | } 203 | // if we didnt read anything then return io.EOF to end gracefully 204 | if n == 0 && len(p) > 0 && err == nil { 205 | err = io.EOF 206 | } 207 | // signal for others that we finished the read 208 | ec.readc.Signal() 209 | return n, err 210 | } 211 | 212 | // Writes len(p) bytes from p to the console. It returns the number of bytes 213 | // written from p (0 <= n <= len(p)) and any error encountered that caused 214 | // the write to stop early. 215 | // 216 | // If writes to the console returns EAGAIN or EIO, we assume that it's a 217 | // temporary error because the other side went away and wait for the signal 218 | // generated by epoll event to continue. 219 | func (ec *EpollConsole) Write(p []byte) (n int, err error) { 220 | var written int 221 | ec.writec.L.Lock() 222 | defer ec.writec.L.Unlock() 223 | for { 224 | written, err = ec.Console.Write(p[n:]) 225 | n += written 226 | if err != nil { 227 | var hangup bool 228 | if perr, ok := err.(*os.PathError); ok { 229 | hangup = (perr.Err == unix.EAGAIN || perr.Err == unix.EIO) 230 | } else { 231 | hangup = (err == unix.EAGAIN || err == unix.EIO) 232 | } 233 | // if the other end disappears, assume this is temporary and wait for the 234 | // signal to continue again. 235 | if hangup { 236 | ec.writec.Wait() 237 | continue 238 | } 239 | } 240 | // unrecoverable error, break the loop and return the error 241 | break 242 | } 243 | if n < len(p) && err == nil { 244 | err = io.ErrShortWrite 245 | } 246 | // signal for others that we finished the write 247 | ec.writec.Signal() 248 | return n, err 249 | } 250 | 251 | // Shutdown closes the file descriptor and signals call waiters for this fd. 252 | // It accepts a callback which will be called with the console's fd. The 253 | // callback typically will be used to do further cleanup such as unregister the 254 | // console's fd from the epoll interface. 255 | // User should call Shutdown and wait for all I/O operation to be finished 256 | // before closing the console. 257 | func (ec *EpollConsole) Shutdown(close func(int) error) error { 258 | ec.readc.L.Lock() 259 | defer ec.readc.L.Unlock() 260 | ec.writec.L.Lock() 261 | defer ec.writec.L.Unlock() 262 | 263 | ec.readc.Broadcast() 264 | ec.writec.Broadcast() 265 | ec.closed = true 266 | return close(ec.sysfd) 267 | } 268 | 269 | // signalRead signals that the console is readable. 270 | func (ec *EpollConsole) signalRead() { 271 | ec.readc.L.Lock() 272 | ec.readc.Signal() 273 | ec.readc.L.Unlock() 274 | } 275 | 276 | // signalWrite signals that the console is writable. 277 | func (ec *EpollConsole) signalWrite() { 278 | ec.writec.L.Lock() 279 | ec.writec.Signal() 280 | ec.writec.L.Unlock() 281 | } 282 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | https://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | Copyright The containerd Authors 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | https://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | --------------------------------------------------------------------------------