refactor: 重构 starssh 核心运行时并补强 ssh/exec/terminal/sftp 能力
- 拆分原有单体 ssh.go,按职责重组为 types、utils、transport、login、keepalive、session、exec、pool、shell、terminal、forward、hostkey、state 等模块,并补充平台相关实现 - 重做登录与连接运行时,补齐基于 context 的建连、jump/proxy 链路、可配置认证顺序,以及 Unix/Windows 下的 ssh-agent 支持 - 新增正式非交互执行模型 ExecRequest/ExecResult,支持流式输出、溢出统计、超时控制,以及 posix/powershell/cmd/raw 多方言执行 - 保留旧 shell 风格兼容接口,同时让路径/用户探测等 helper 具备跨 shell fallback,避免 Windows 目标继续硬依赖 POSIX 命令 - 新增 TerminalSession 作为原始交互终端基座,提供 IO attach、resize、signal/control、退出状态与关闭原因管理 - 重构端口转发语义,默认复用当前 SSH 连接,并显式提供 detached 的本地/动态转发模式承载隔离场景 - 梳理 keepalive 与取消语义,区分仅取消本次操作和关闭整条连接,并统一连接状态与传输关闭路径 - 围绕新的 session/连接生命周期重做执行池与运行时支撑 - 大幅增强 SFTP 传输链路,补齐更安全的原子替换、校验、进度回调、重试隔离、可复用 client 生命周期与失败语义 - 新增取消语义、keepalive、SFTP、forward、terminal input 等关键回归测试,提升核心链路稳定性
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
package starssh
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"sync"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// TerminalInputSourceProvider lets wrapper readers expose a closer-friendly source reader.
|
||||
// Implementations that buffer data should return a source that already includes any prefetched bytes.
|
||||
type TerminalInputSourceProvider interface {
|
||||
TerminalInputSource() io.Reader
|
||||
}
|
||||
|
||||
// TerminalInputCanceler lets wrapper readers expose an explicit cancellation hook.
|
||||
// It is useful for line editors or custom buffered readers that cannot safely expose a raw io.ReadCloser.
|
||||
type TerminalInputCanceler interface {
|
||||
TerminalInputCancel() error
|
||||
}
|
||||
|
||||
// TerminalInputAdapter adapts wrapper readers into a cancelable terminal input source.
|
||||
// Reader is what TerminalSession consumes, Source is the closer-friendly underlying reader when available.
|
||||
type TerminalInputAdapter struct {
|
||||
Reader io.Reader
|
||||
Source io.Reader
|
||||
Cancel func() error
|
||||
}
|
||||
|
||||
func (a TerminalInputAdapter) Read(p []byte) (int, error) {
|
||||
if a.Reader == nil {
|
||||
return 0, io.EOF
|
||||
}
|
||||
return a.Reader.Read(p)
|
||||
}
|
||||
|
||||
func (a TerminalInputAdapter) TerminalInputSource() io.Reader {
|
||||
if a.Source != nil {
|
||||
return a.Source
|
||||
}
|
||||
return a.Reader
|
||||
}
|
||||
|
||||
func (a TerminalInputAdapter) TerminalInputCancel() error {
|
||||
if a.Cancel != nil {
|
||||
return a.Cancel()
|
||||
}
|
||||
if closer, ok := a.Source.(io.Closer); ok && closer != nil {
|
||||
return closer.Close()
|
||||
}
|
||||
if closer, ok := a.Reader.(io.Closer); ok && closer != nil {
|
||||
return closer.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func prepareTerminalInputReader(in io.Reader) (io.Reader, func(), bool, error) {
|
||||
if in == nil {
|
||||
return nil, func() {}, false, nil
|
||||
}
|
||||
|
||||
var cancelOnce sync.Once
|
||||
wrapCancel := func(fn func()) func() {
|
||||
return func() {
|
||||
cancelOnce.Do(fn)
|
||||
}
|
||||
}
|
||||
|
||||
if provider, ok := in.(TerminalInputSourceProvider); ok {
|
||||
source := provider.TerminalInputSource()
|
||||
if source == nil || sameReader(source, in) {
|
||||
return prepareDirectTerminalInputReader(in, wrapCancel)
|
||||
}
|
||||
|
||||
prepared, cancel, cancelable, err := prepareTerminalInputReader(source)
|
||||
if err != nil {
|
||||
return nil, nil, false, err
|
||||
}
|
||||
if canceler, ok := in.(TerminalInputCanceler); ok {
|
||||
return prepared, wrapCancel(func() {
|
||||
cancel()
|
||||
_ = canceler.TerminalInputCancel()
|
||||
}), true, nil
|
||||
}
|
||||
return prepared, cancel, cancelable, nil
|
||||
}
|
||||
|
||||
return prepareDirectTerminalInputReader(in, wrapCancel)
|
||||
}
|
||||
|
||||
func prepareDirectTerminalInputReader(in io.Reader, wrapCancel func(func()) func()) (io.Reader, func(), bool, error) {
|
||||
if in == nil {
|
||||
return nil, func() {}, false, nil
|
||||
}
|
||||
|
||||
switch typed := in.(type) {
|
||||
case *bufio.Reader:
|
||||
return prepareBufferedTerminalInputReader(typed)
|
||||
case *bufio.ReadWriter:
|
||||
if typed.Reader == nil {
|
||||
return in, func() {}, false, nil
|
||||
}
|
||||
return prepareBufferedTerminalInputReader(typed.Reader)
|
||||
}
|
||||
|
||||
if canceler, ok := in.(TerminalInputCanceler); ok {
|
||||
return in, wrapCancel(func() {
|
||||
_ = canceler.TerminalInputCancel()
|
||||
}), true, nil
|
||||
}
|
||||
|
||||
if file, ok := in.(*os.File); ok {
|
||||
dup, err := duplicateTerminalInputFile(file)
|
||||
if err != nil {
|
||||
return nil, nil, false, fmt.Errorf("duplicate terminal input: %w", err)
|
||||
}
|
||||
return dup, wrapCancel(func() {
|
||||
_ = dup.Close()
|
||||
}), true, nil
|
||||
}
|
||||
|
||||
if closer, ok := in.(io.ReadCloser); ok {
|
||||
return closer, wrapCancel(func() {
|
||||
_ = closer.Close()
|
||||
}), true, nil
|
||||
}
|
||||
|
||||
return in, func() {}, false, nil
|
||||
}
|
||||
|
||||
func prepareBufferedTerminalInputReader(reader *bufio.Reader) (io.Reader, func(), bool, error) {
|
||||
if reader == nil {
|
||||
return nil, func() {}, false, nil
|
||||
}
|
||||
|
||||
bufferedPrefix, err := snapshotBufferedPrefix(reader)
|
||||
if err != nil {
|
||||
return nil, nil, false, err
|
||||
}
|
||||
|
||||
underlying := unwrapBufioReader(reader)
|
||||
if underlying == nil {
|
||||
if len(bufferedPrefix) == 0 {
|
||||
return reader, func() {}, false, nil
|
||||
}
|
||||
return io.MultiReader(bytes.NewReader(bufferedPrefix), reader), func() {}, false, nil
|
||||
}
|
||||
|
||||
prepared, cancel, cancelable, err := prepareTerminalInputReader(underlying)
|
||||
if err != nil {
|
||||
return nil, nil, false, err
|
||||
}
|
||||
if len(bufferedPrefix) == 0 {
|
||||
return prepared, cancel, cancelable, nil
|
||||
}
|
||||
if prepared == nil {
|
||||
return bytes.NewReader(bufferedPrefix), cancel, cancelable, nil
|
||||
}
|
||||
return io.MultiReader(bytes.NewReader(bufferedPrefix), prepared), cancel, cancelable, nil
|
||||
}
|
||||
|
||||
func snapshotBufferedPrefix(reader *bufio.Reader) ([]byte, error) {
|
||||
if reader == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
buffered := reader.Buffered()
|
||||
if buffered == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
chunk, err := reader.Peek(buffered)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil, fmt.Errorf("peek terminal input buffer: %w", err)
|
||||
}
|
||||
prefix := append([]byte(nil), chunk...)
|
||||
if _, err := reader.Discard(len(prefix)); err != nil {
|
||||
return nil, fmt.Errorf("discard terminal input buffer: %w", err)
|
||||
}
|
||||
return prefix, nil
|
||||
}
|
||||
|
||||
func unwrapBufioReader(reader *bufio.Reader) io.Reader {
|
||||
if reader == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
value := reflect.ValueOf(reader)
|
||||
if value.Kind() != reflect.Pointer || value.IsNil() {
|
||||
return nil
|
||||
}
|
||||
|
||||
field := value.Elem().FieldByName("rd")
|
||||
if !field.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
underlyingValue := reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem()
|
||||
underlying, ok := underlyingValue.Interface().(io.Reader)
|
||||
if !ok || underlying == nil || sameReader(underlying, reader) {
|
||||
return nil
|
||||
}
|
||||
return underlying
|
||||
}
|
||||
|
||||
func sameReader(left io.Reader, right io.Reader) bool {
|
||||
if left == nil || right == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
leftValue := reflect.ValueOf(left)
|
||||
rightValue := reflect.ValueOf(right)
|
||||
if !leftValue.IsValid() || !rightValue.IsValid() {
|
||||
return false
|
||||
}
|
||||
if leftValue.Kind() != reflect.Pointer || rightValue.Kind() != reflect.Pointer {
|
||||
return false
|
||||
}
|
||||
return leftValue.Pointer() == rightValue.Pointer()
|
||||
}
|
||||
Reference in New Issue
Block a user