修复: 粘贴图失效与SFTP串台回退等一批问题
粘贴图根修(Ctrl+V直读系统剪贴板,不再依赖不可靠的paste事件)/SFTP串台双刀(拨号期禁回退本机+盘符路径自动回根)/PDF导出目录设置与文案带落盘路径/性能批(连接保存下载防重/md渲染防抖+预热/hljs按需加载/审计锁外置/快捷方式解析超时/webview-watch可停)/前端62类型错清零(含keyPath蛇形字段炸弹)/目录图标字段错位修复
This commit is contained in:
+30
-12
@@ -10,11 +10,16 @@ import (
|
||||
|
||||
"github.com/pkg/sftp"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"u-desk/internal/common"
|
||||
)
|
||||
|
||||
// 连接建立全流程(拨号+认证+SFTP 子系统)的总超时预算
|
||||
const connectTimeout = 10 * time.Second
|
||||
|
||||
// 拨号独立预算,小于总预算:冷连接卡死大多发生在 TCP 拨号,
|
||||
// 独立预算保证握手认证阶段始终有剩余 deadline 可用
|
||||
const dialTimeout = 6 * time.Second
|
||||
|
||||
// 保活参数:周期发送 keepalive 并等待回复,超过 keepaliveReplyWait 未回复判定连接已死
|
||||
const (
|
||||
keepaliveInterval = 30 * time.Second
|
||||
@@ -45,7 +50,8 @@ func GetManager() *Manager {
|
||||
}
|
||||
|
||||
// Connect 创建或复用 SFTP 连接
|
||||
func (m *Manager) Connect(config *Config) (*Client, error) {
|
||||
// onStage 在连接各阶段推进时回调(dial/auth/channel),可为 nil
|
||||
func (m *Manager) Connect(config *Config, onStage func(stage string)) (*Client, error) {
|
||||
key := fmt.Sprintf("%s:%d", config.Host, config.Port)
|
||||
|
||||
m.mu.Lock()
|
||||
@@ -60,7 +66,7 @@ func (m *Manager) Connect(config *Config) (*Client, error) {
|
||||
m.clients.Delete(key)
|
||||
}
|
||||
|
||||
c, err := newClient(config)
|
||||
c, err := newClient(config, onStage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -102,8 +108,8 @@ func (m *Manager) Shutdown() {
|
||||
// --- 内部 ---
|
||||
|
||||
// newClient 建立连接并启动保活循环
|
||||
func newClient(config *Config) (*Client, error) {
|
||||
c, err := buildClient(config)
|
||||
func newClient(config *Config, onStage func(stage string)) (*Client, error) {
|
||||
c, err := buildClient(config, onStage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -112,7 +118,14 @@ func newClient(config *Config) (*Client, error) {
|
||||
}
|
||||
|
||||
// buildClient 拨号+认证+SFTP 子系统建立(全程受 connectTimeout 约束),不启动保活
|
||||
func buildClient(config *Config) (*Client, error) {
|
||||
// onStage 标记各阶段开始:dial=拨号、auth=认证、channel=建通道;SSH 协议链路串行,
|
||||
// 无法并行加速,分步回调让前端可见当前卡点
|
||||
func buildClient(config *Config, onStage func(stage string)) (*Client, error) {
|
||||
notify := func(stage string) {
|
||||
if onStage != nil {
|
||||
onStage(stage)
|
||||
}
|
||||
}
|
||||
sshConfig := &ssh.ClientConfig{
|
||||
Config: ssh.Config{
|
||||
KeyExchanges: []string{
|
||||
@@ -160,14 +173,17 @@ func buildClient(config *Config) (*Client, error) {
|
||||
|
||||
// JoinHostPort 保证 IPv6 主机地址被方括号包裹,fmt 拼接对 IPv6 产生非法地址
|
||||
addr := net.JoinHostPort(config.Host, strconv.Itoa(config.Port))
|
||||
// 拨号与认证、SFTP 子系统握手共享同一总超时预算,认证阶段无界挂起由 deadline 掐断
|
||||
// 总 deadline 从拨号前起算:拨号占独立 6s 预算,认证与 SFTP 子系统握手受剩余总预算约束,
|
||||
// 认证阶段无界挂起由 deadline 掐断
|
||||
deadline := time.Now().Add(connectTimeout)
|
||||
sshConn, err := net.DialTimeout("tcp", addr, connectTimeout)
|
||||
notify("dial")
|
||||
sshConn, err := net.DialTimeout("tcp", addr, dialTimeout)
|
||||
if err != nil {
|
||||
return nil, &ConnectionError{Op: "dial", Err: err}
|
||||
}
|
||||
sshConn.SetDeadline(deadline)
|
||||
|
||||
notify("auth")
|
||||
sshConnConn, chans, reqs, err := ssh.NewClientConn(sshConn, addr, sshConfig)
|
||||
if err != nil {
|
||||
sshConn.Close()
|
||||
@@ -176,6 +192,7 @@ func buildClient(config *Config) (*Client, error) {
|
||||
|
||||
sshClient := ssh.NewClient(sshConnConn, chans, reqs)
|
||||
|
||||
notify("channel")
|
||||
sftpClient, err := sftp.NewClient(sshClient)
|
||||
sshConn.SetDeadline(time.Time{})
|
||||
if err != nil {
|
||||
@@ -196,7 +213,7 @@ func (c *Client) startKeepalive() {
|
||||
stop := make(chan struct{})
|
||||
sshClient := c.sshClient
|
||||
c.stopKeep = stop
|
||||
go func() {
|
||||
common.Go("sftp-keepalive", func() {
|
||||
t := time.NewTicker(keepaliveInterval)
|
||||
defer t.Stop()
|
||||
for {
|
||||
@@ -210,16 +227,16 @@ func (c *Client) startKeepalive() {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// sshKeepalive 发送一次 keepalive 请求并限时等待回复,超时或出错视作连接已死
|
||||
func sshKeepalive(sshClient *ssh.Client, wait time.Duration) bool {
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
common.Go("sftp-keepalive-probe", func() {
|
||||
_, _, err := sshClient.SendRequest("keepalive@openssh.com", true, nil)
|
||||
done <- err
|
||||
}()
|
||||
})
|
||||
select {
|
||||
case err := <-done:
|
||||
return err == nil
|
||||
@@ -295,7 +312,8 @@ func (c *Client) WithRetry(fn func(*sftp.Client) error) error {
|
||||
|
||||
func (c *Client) reconnect() error {
|
||||
// 用 buildClient 而非 newClient,避免保活循环绑定到即将丢弃的临时容器
|
||||
nc, err := buildClient(c.config)
|
||||
// 后台重连无阶段展示需求,回调传 nil
|
||||
nc, err := buildClient(c.config, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -844,9 +844,9 @@ func (s *Service) GetSystemInfo(connID string) (map[string]interface{}, error) {
|
||||
out, e := c.RunCommand(cmd)
|
||||
results <- cmdResult{key, out, e}
|
||||
}
|
||||
go runCmd("df", "df -B1 / 2>/dev/null | tail -1")
|
||||
go runCmd("mem", "free -b 2>/dev/null | grep -E '^Mem:' || cat /proc/meminfo 2>/dev/null | head -2")
|
||||
go runCmd("cpu", "top -bn1 | grep 'Cpu(s)' | awk '{print $2}' | cut -d'%' -f1")
|
||||
common.Go("sftp-cmd-df", func() { runCmd("df", "df -B1 / 2>/dev/null | tail -1") })
|
||||
common.Go("sftp-cmd-mem", func() { runCmd("mem", "free -b 2>/dev/null | grep -E '^Mem:' || cat /proc/meminfo 2>/dev/null | head -2") })
|
||||
common.Go("sftp-cmd-cpu", func() { runCmd("cpu", "top -bn1 | grep 'Cpu(s)' | awk '{print $2}' | cut -d'%' -f1") })
|
||||
|
||||
var dfOut, memOut, cpuOut string
|
||||
hasErr := false
|
||||
|
||||
Reference in New Issue
Block a user