working commit

This commit is contained in:
2026-03-13 19:02:42 +02:00
parent bebbf79c7a
commit 5c1da77f4c
1329 changed files with 314708 additions and 39 deletions
+148
View File
@@ -0,0 +1,148 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package term
import (
"fmt"
"github.com/moby/term"
"k8s.io/apimachinery/pkg/util/runtime"
)
// TerminalSize represents the width and height of a terminal.
// It is the same as staging/src/k8s.io/client-go/tools/remotecommand.TerminalSize.
// Copied to decouple the packages. Terminal-related package should not depend on API client and vice versa.
type TerminalSize struct {
Width uint16
Height uint16
}
// TerminalSizeQueue is capable of returning terminal resize events as they occur.
// It is the same as staging/src/k8s.io/client-go/tools/remotecommand.TerminalSizeQueue.
// Copied to decouple the packages. Terminal-related package should not depend on API client and vice versa.
type TerminalSizeQueue interface {
// Next returns the new terminal size after the terminal has been resized. It returns nil when
// monitoring has been stopped.
Next() *TerminalSize
}
// GetSize returns the current size of the user's terminal. If it isn't a terminal,
// nil is returned.
func (t TTY) GetSize() *TerminalSize {
outFd, isTerminal := term.GetFdInfo(t.Out)
if !isTerminal {
return nil
}
return GetSize(outFd)
}
// GetSize returns the current size of the terminal associated with fd.
func GetSize(fd uintptr) *TerminalSize {
winsize, err := term.GetWinsize(fd)
if err != nil {
runtime.HandleError(fmt.Errorf("unable to get terminal size: %v", err))
return nil
}
return &TerminalSize{Width: winsize.Width, Height: winsize.Height}
}
// MonitorSize monitors the terminal's size. It returns a TerminalSizeQueue primed with
// initialSizes, or nil if there's no TTY present.
func (t *TTY) MonitorSize(initialSizes ...*TerminalSize) TerminalSizeQueue {
outFd, isTerminal := term.GetFdInfo(t.Out)
if !isTerminal {
return nil
}
t.sizeQueue = &sizeQueue{
t: *t,
// make it buffered so we can send the initial terminal sizes without blocking, prior to starting
// the streaming below
resizeChan: make(chan TerminalSize, len(initialSizes)),
stopResizing: make(chan struct{}),
}
t.sizeQueue.monitorSize(outFd, initialSizes...)
return t.sizeQueue
}
// sizeQueue implements remotecommand.TerminalSizeQueue
type sizeQueue struct {
t TTY
// resizeChan receives a Size each time the user's terminal is resized.
resizeChan chan TerminalSize
stopResizing chan struct{}
}
// make sure sizeQueue implements the TerminalSizeQueue interface
var _ TerminalSizeQueue = &sizeQueue{}
// monitorSize primes resizeChan with initialSizes and then monitors for resize events. With each
// new event, it sends the current terminal size to resizeChan.
func (s *sizeQueue) monitorSize(outFd uintptr, initialSizes ...*TerminalSize) {
// send the initial sizes
for i := range initialSizes {
if initialSizes[i] != nil {
s.resizeChan <- *initialSizes[i]
}
}
resizeEvents := make(chan TerminalSize, 1)
monitorResizeEvents(outFd, resizeEvents, s.stopResizing)
// listen for resize events in the background
go func() {
defer runtime.HandleCrash()
for {
select {
case size, ok := <-resizeEvents:
if !ok {
return
}
select {
// try to send the size to resizeChan, but don't block
case s.resizeChan <- size:
// send successful
default:
// unable to send / no-op
}
case <-s.stopResizing:
return
}
}
}()
}
// Next returns the new terminal size after the terminal has been resized. It returns nil when
// monitoring has been stopped.
func (s *sizeQueue) Next() *TerminalSize {
size, ok := <-s.resizeChan
if !ok {
return nil
}
return &size
}
// stop stops the background goroutine that is monitoring for terminal resizes.
func (s *sizeQueue) stop() {
close(s.stopResizing)
}
+61
View File
@@ -0,0 +1,61 @@
//go:build !windows
// +build !windows
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package term
import (
"os"
"os/signal"
"golang.org/x/sys/unix"
"k8s.io/apimachinery/pkg/util/runtime"
)
// monitorResizeEvents spawns a goroutine that waits for SIGWINCH signals (these indicate the
// terminal has resized). After receiving a SIGWINCH, this gets the terminal size and tries to send
// it to the resizeEvents channel. The goroutine stops when the stop channel is closed.
func monitorResizeEvents(fd uintptr, resizeEvents chan<- TerminalSize, stop chan struct{}) {
go func() {
defer runtime.HandleCrash()
winch := make(chan os.Signal, 1)
signal.Notify(winch, unix.SIGWINCH)
defer signal.Stop(winch)
for {
select {
case <-winch:
size := GetSize(fd)
if size == nil {
return
}
// try to send size
select {
case resizeEvents <- *size:
// success
default:
// not sent
}
case <-stop:
return
}
}
}()
}
+61
View File
@@ -0,0 +1,61 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package term
import (
"time"
"k8s.io/apimachinery/pkg/util/runtime"
)
// monitorResizeEvents spawns a goroutine that periodically gets the terminal size and tries to send
// it to the resizeEvents channel if the size has changed. The goroutine stops when the stop channel
// is closed.
func monitorResizeEvents(fd uintptr, resizeEvents chan<- TerminalSize, stop chan struct{}) {
go func() {
defer runtime.HandleCrash()
size := GetSize(fd)
if size == nil {
return
}
lastSize := *size
for {
// see if we need to stop running
select {
case <-stop:
return
default:
}
size := GetSize(fd)
if size == nil {
return
}
if size.Height != lastSize.Height || size.Width != lastSize.Width {
lastSize.Height = size.Height
lastSize.Width = size.Width
resizeEvents <- *size
}
// sleep to avoid hot looping
time.Sleep(250 * time.Millisecond)
}
}()
}
+115
View File
@@ -0,0 +1,115 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package term
import (
"io"
"os"
"k8s.io/cli-runtime/pkg/printers"
"github.com/moby/term"
"k8s.io/kubectl/pkg/util/interrupt"
)
// SafeFunc is a function to be invoked by TTY.
type SafeFunc func() error
// TTY helps invoke a function and preserve the state of the terminal, even if the process is
// terminated during execution. It also provides support for terminal resizing for remote command
// execution/attachment.
type TTY struct {
// In is a reader representing stdin. It is a required field.
In io.Reader
// Out is a writer representing stdout. It must be set to support terminal resizing. It is an
// optional field.
Out io.Writer
// Raw is true if the terminal should be set raw.
Raw bool
// TryDev indicates the TTY should try to open /dev/tty if the provided input
// is not a file descriptor.
TryDev bool
// Parent is an optional interrupt handler provided to this function - if provided
// it will be invoked after the terminal state is restored. If it is not provided,
// a signal received during the TTY will result in os.Exit(0) being invoked.
Parent *interrupt.Handler
// sizeQueue is set after a call to MonitorSize() and is used to monitor SIGWINCH signals when the
// user's terminal resizes.
sizeQueue *sizeQueue
}
// IsTerminalIn returns true if t.In is a terminal. Does not check /dev/tty
// even if TryDev is set.
func (t TTY) IsTerminalIn() bool {
return printers.IsTerminal(t.In)
}
// IsTerminalOut returns true if t.Out is a terminal. Does not check /dev/tty
// even if TryDev is set.
func (t TTY) IsTerminalOut() bool {
return printers.IsTerminal(t.Out)
}
// IsTerminal returns whether the passed object is a terminal or not.
// Deprecated: use printers.IsTerminal instead.
var IsTerminal = printers.IsTerminal
// AllowsColorOutput returns true if the specified writer is a terminal and
// the process environment indicates color output is supported and desired.
// Deprecated: use printers.AllowsColorOutput instead.
var AllowsColorOutput = printers.AllowsColorOutput
// Safe invokes the provided function and will attempt to ensure that when the
// function returns (or a termination signal is sent) that the terminal state
// is reset to the condition it was in prior to the function being invoked. If
// t.Raw is true the terminal will be put into raw mode prior to calling the function.
// If the input file descriptor is not a TTY and TryDev is true, the /dev/tty file
// will be opened (if available).
func (t TTY) Safe(fn SafeFunc) error {
inFd, isTerminal := term.GetFdInfo(t.In)
if !isTerminal && t.TryDev {
if f, err := os.Open("/dev/tty"); err == nil {
defer f.Close()
inFd = f.Fd()
isTerminal = term.IsTerminal(inFd)
}
}
if !isTerminal {
return fn()
}
var state *term.State
var err error
if t.Raw {
state, err = term.MakeRaw(inFd)
} else {
state, err = term.SaveState(inFd)
}
if err != nil {
return err
}
return interrupt.Chain(t.Parent, func() {
if t.sizeQueue != nil {
t.sizeQueue.stop()
}
term.RestoreTerminal(inFd, state)
}).Run(fn)
}
+144
View File
@@ -0,0 +1,144 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package term
import (
"errors"
"io"
"os"
wordwrap "github.com/mitchellh/go-wordwrap"
"github.com/moby/term"
)
type wordWrapWriter struct {
limit uint
writer io.Writer
}
// NewResponsiveWriter creates a Writer that detects the column width of the
// terminal we are in, and adjusts every line width to fit and use recommended
// terminal sizes for better readability. Does proper word wrapping automatically.
//
// if terminal width >= 120 columns use 120 columns
// if terminal width >= 100 columns use 100 columns
// if terminal width >= 80 columns use 80 columns
//
// In case we're not in a terminal or if it's smaller than 80 columns width,
// doesn't do any wrapping.
func NewResponsiveWriter(w io.Writer) io.Writer {
file, ok := w.(*os.File)
if !ok {
return w
}
fd := file.Fd()
if !term.IsTerminal(fd) {
return w
}
terminalSize := GetSize(fd)
if terminalSize == nil {
return w
}
limit := getTerminalLimitWidth(terminalSize)
return NewWordWrapWriter(w, limit)
}
// NewWordWrapWriter is a Writer that supports a limit of characters on every line
// and does auto word wrapping that respects that limit.
func NewWordWrapWriter(w io.Writer, limit uint) io.Writer {
return &wordWrapWriter{
limit: limit,
writer: w,
}
}
func getTerminalLimitWidth(terminalSize *TerminalSize) uint {
var limit uint
switch {
case terminalSize.Width >= 120:
limit = 120
case terminalSize.Width >= 100:
limit = 100
case terminalSize.Width >= 80:
limit = 80
}
return limit
}
func GetWordWrapperLimit() (uint, error) {
stdout := os.Stdout
fd := stdout.Fd()
if !term.IsTerminal(fd) {
return 0, errors.New("file descriptor is not a terminal")
}
terminalSize := GetSize(fd)
if terminalSize == nil {
return 0, errors.New("terminal size is nil")
}
return getTerminalLimitWidth(terminalSize), nil
}
func (w wordWrapWriter) Write(p []byte) (nn int, err error) {
if w.limit == 0 {
return w.writer.Write(p)
}
original := string(p)
wrapped := wordwrap.WrapString(original, w.limit)
return w.writer.Write([]byte(wrapped))
}
// NewPunchCardWriter is a NewWordWrapWriter that limits the line width to 80 columns.
func NewPunchCardWriter(w io.Writer) io.Writer {
return NewWordWrapWriter(w, 80)
}
type maxWidthWriter struct {
maxWidth uint
currentWidth uint
written uint
writer io.Writer
}
// NewMaxWidthWriter is a Writer that supports a limit of characters on every
// line, but doesn't do any word wrapping automatically.
func NewMaxWidthWriter(w io.Writer, maxWidth uint) io.Writer {
return &maxWidthWriter{
maxWidth: maxWidth,
writer: w,
}
}
func (m maxWidthWriter) Write(p []byte) (nn int, err error) {
for _, b := range p {
if m.currentWidth == m.maxWidth {
m.writer.Write([]byte{'\n'})
m.currentWidth = 0
}
if b == '\n' {
m.currentWidth = 0
}
_, err := m.writer.Write([]byte{b})
if err != nil {
return int(m.written), err
}
m.written++
m.currentWidth++
}
return len(p), nil
}