修复: 粘贴图失效与SFTP串台回退等一批问题
粘贴图根修(Ctrl+V直读系统剪贴板,不再依赖不可靠的paste事件)/SFTP串台双刀(拨号期禁回退本机+盘符路径自动回根)/PDF导出目录设置与文案带落盘路径/性能批(连接保存下载防重/md渲染防抖+预热/hljs按需加载/审计锁外置/快捷方式解析超时/webview-watch可停)/前端62类型错清零(含keyPath蛇形字段炸弹)/目录图标字段错位修复
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"u-desk/internal/common"
|
||||
"u-desk/internal/service"
|
||||
)
|
||||
|
||||
@@ -133,7 +134,7 @@ func (api *UpdateAPI) DownloadUpdate(downloadURL string) (map[string]interface{}
|
||||
return errorResponse("已有下载任务进行中"), nil
|
||||
}
|
||||
|
||||
go func() {
|
||||
common.Go("download-update", func() {
|
||||
defer api.downloading.Store(false)
|
||||
|
||||
progressCallback := func(progress float64, speed float64, downloaded int64, total int64) {
|
||||
@@ -162,7 +163,7 @@ func (api *UpdateAPI) DownloadUpdate(downloadURL string) (map[string]interface{}
|
||||
}
|
||||
api.emit("download-complete", resultInfo)
|
||||
}
|
||||
}()
|
||||
})
|
||||
|
||||
return successResponse(map[string]interface{}{
|
||||
"message": "下载已开始",
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GetLogsDir 日志目录:~/.u-desk/logs(audit 日志同目录)
|
||||
func GetLogsDir() string {
|
||||
return filepath.Join(GetUserDataDir(), "logs")
|
||||
}
|
||||
|
||||
// dailyFile 按天滚动的追加写文件:每次写时发现日期变化即切换新文件。
|
||||
// 只做按天滚动,不搞大小上限/压缩/清理,崩溃取证够用。
|
||||
type dailyFile struct {
|
||||
mu sync.Mutex
|
||||
dir string
|
||||
naming func(t time.Time) string
|
||||
f *os.File
|
||||
day string
|
||||
}
|
||||
|
||||
func newDailyFile(dir string, naming func(time.Time) string) *dailyFile {
|
||||
return &dailyFile{dir: dir, naming: naming}
|
||||
}
|
||||
|
||||
func (w *dailyFile) Write(p []byte) (int, error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
day := now.Format("2006-01-02")
|
||||
if w.f != nil && day != w.day {
|
||||
w.f.Close()
|
||||
w.f = nil
|
||||
}
|
||||
if w.f == nil {
|
||||
if err := os.MkdirAll(w.dir, 0755); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
f, err := os.OpenFile(filepath.Join(w.dir, w.naming(now)), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
w.f, w.day = f, day
|
||||
}
|
||||
return w.f.Write(p)
|
||||
}
|
||||
|
||||
func (w *dailyFile) Close() error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
if w.f == nil {
|
||||
return nil
|
||||
}
|
||||
err := w.f.Close()
|
||||
w.f = nil
|
||||
return err
|
||||
}
|
||||
|
||||
// FileLogOut 应用文件日志的底层写出器(stderr + 按天文件 MultiWriter)。
|
||||
// SetupFileLogging 之后有效;供需要独立过滤级别的 logger 复用同一落盘目的地
|
||||
// (如 wails 系统日志降到 Warn,业务 slog.Default() 保持 INFO 全量)。
|
||||
var FileLogOut io.Writer = os.Stderr
|
||||
|
||||
// SetupFileLogging slog 双路落盘:stderr + ~/.u-desk/logs/app-YYYY-MM-DD.log。
|
||||
// windowsgui 下 stderr 被丢弃,文件路是日志唯一可见载体;
|
||||
// 文件侧打不开时静默降级为仅 stderr,不因日志失败阻断启动。
|
||||
func SetupFileLogging() {
|
||||
out := io.Writer(os.Stderr)
|
||||
w := newDailyFile(GetLogsDir(), func(t time.Time) string {
|
||||
return fmt.Sprintf("app-%s.log", t.Format("2006-01-02"))
|
||||
})
|
||||
// 预开当天文件探测文件侧可用性(Write 内部含懒开/换天逻辑)
|
||||
if _, err := w.Write(nil); err == nil {
|
||||
// 文件必须放 MultiWriter 首位:windowsgui 下 stderr 句柄无效,
|
||||
// MultiWriter 按序短路,stderr 在前会吞掉后续文件路(真机零日志的根因)
|
||||
out = io.MultiWriter(w, os.Stderr)
|
||||
} else {
|
||||
w.Close()
|
||||
}
|
||||
FileLogOut = out
|
||||
slog.SetDefault(slog.New(slog.NewTextHandler(out, nil)))
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var panicLogMu sync.Mutex
|
||||
|
||||
// AppendPanicLog 把一次 panic 的来源名、值与完整栈追加写入
|
||||
// ~/.u-desk/logs/panic-YYYYMMDD.log。写失败静默放弃:取证失败不能引发二次故障。
|
||||
func AppendPanicLog(name string, r any) {
|
||||
dir := GetLogsDir()
|
||||
panicLogMu.Lock()
|
||||
defer panicLogMu.Unlock()
|
||||
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return
|
||||
}
|
||||
path := filepath.Join(dir, fmt.Sprintf("panic-%s.log", time.Now().Format("20060102")))
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
// debug.Stack 在 defer 内 panic 未展开时调用,包含 panic 原始调用栈
|
||||
fmt.Fprintf(f, "[%s] goroutine=%s panic=%v\n%s\n",
|
||||
time.Now().Format("2006-01-02 15:04:05.000"), name, r, debug.Stack())
|
||||
}
|
||||
|
||||
// Go 带 panic 兜底的 goroutine:panic 时写完整取证后仅结束该 goroutine,
|
||||
// 不让单点 panic 杀掉整个进程(windowsgui 下默认静默退出零痕迹)。
|
||||
func Go(name string, fn func()) {
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
AppendPanicLog(name, r)
|
||||
}
|
||||
}()
|
||||
fn()
|
||||
}()
|
||||
}
|
||||
@@ -199,10 +199,11 @@ func RunConcurrent[T any](tasks []T, limit int, fn func(t T)) {
|
||||
for _, task := range tasks {
|
||||
sem <- struct{}{}
|
||||
wg.Add(1)
|
||||
go func(t T) {
|
||||
t := task
|
||||
Go("bounded-parallel", func() {
|
||||
defer func() { <-sem; wg.Done() }()
|
||||
fn(t)
|
||||
}(task)
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"u-desk/internal/common"
|
||||
)
|
||||
|
||||
const DefaultFileServerPort = 2652
|
||||
@@ -142,11 +144,11 @@ func listenWithFallback(basePort int, handler http.Handler) (addr string, srv *h
|
||||
l, e := net.Listen("tcp", addr)
|
||||
if e == nil {
|
||||
srv = &http.Server{Handler: handler}
|
||||
go func() {
|
||||
common.Go("local-file-server-serve", func() {
|
||||
if se := srv.Serve(l); se != http.ErrServerClosed {
|
||||
log.Printf("[LocalFileServer] 异常退出: %v", se)
|
||||
}
|
||||
}()
|
||||
})
|
||||
return addr, srv, nil
|
||||
}
|
||||
log.Printf("[LocalFileServer] 端口 %d 被占用,尝试 %d...", port, port+1)
|
||||
@@ -346,11 +348,11 @@ type LocalFileHandler struct {
|
||||
// NewLocalFileHandler 创建本地文件处理器
|
||||
func NewLocalFileHandler() *LocalFileHandler {
|
||||
// 启动本地文件服务器
|
||||
go func() {
|
||||
common.Go("local-file-handler-start", func() {
|
||||
if _, err := StartLocalFileServer(); err != nil {
|
||||
log.Printf("[LocalFileHandler] 启动本地文件服务器失败: %v", err)
|
||||
}
|
||||
}()
|
||||
})
|
||||
return &LocalFileHandler{}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,14 @@ package filesystem
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"u-desk/internal/common"
|
||||
)
|
||||
|
||||
// AuditOperation 审计操作类型
|
||||
@@ -78,7 +81,7 @@ func (a *AuditLogger) ensureReady() error {
|
||||
}
|
||||
a.logFile = f
|
||||
a.initialized = true
|
||||
go a.backgroundFlush()
|
||||
common.Go("audit-flush", a.backgroundFlush)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -102,10 +105,14 @@ func (a *AuditLogger) Log(entry AuditLogEntry) error {
|
||||
// 添加到缓冲区
|
||||
a.buffer = append(a.buffer, entry)
|
||||
|
||||
// 如果缓冲区满了,立即写入
|
||||
// 如果缓冲区满了,立即写入(缓冲满即刷的热路径语义不变:写+Sync 在已持有的锁内完成)
|
||||
if len(a.buffer) >= a.bufferSize {
|
||||
if err := a.flush(); err != nil {
|
||||
if f, err := a.flush(); err != nil {
|
||||
return err
|
||||
} else if f != nil {
|
||||
if err := f.Sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,10 +173,11 @@ func (a *AuditLogger) LogRead(path string, size int64, err error) {
|
||||
_ = a.Log(entry)
|
||||
}
|
||||
|
||||
// flush 将缓冲区写入文件
|
||||
func (a *AuditLogger) flush() error {
|
||||
// flush 将缓冲区写入文件(调用方须持有 a.mu),返回刚写入的文件供调用方在锁外 Sync。
|
||||
// Sync 是慢速磁盘操作,不放在锁内执行,避免阻塞 Log 热路径。
|
||||
func (a *AuditLogger) flush() (*os.File, error) {
|
||||
if len(a.buffer) == 0 {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// 序列化所有条目为JSON(每行一个)
|
||||
@@ -179,19 +187,25 @@ func (a *AuditLogger) flush() error {
|
||||
continue // 序列化失败,跳过该条目
|
||||
}
|
||||
if _, err := a.logFile.Write(append(data, '\n')); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新到磁盘
|
||||
if err := a.logFile.Sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 清空缓冲区
|
||||
a.buffer = a.buffer[:0]
|
||||
|
||||
return nil
|
||||
return a.logFile, nil
|
||||
}
|
||||
|
||||
// flushSynced 锁内写缓冲、锁外 Sync:fsync 不占 a.mu,后台定时刷盘不再阻塞 Log
|
||||
func (a *AuditLogger) flushSynced() error {
|
||||
a.mu.Lock()
|
||||
f, err := a.flush()
|
||||
a.mu.Unlock()
|
||||
if err != nil || f == nil {
|
||||
return err
|
||||
}
|
||||
return f.Sync()
|
||||
}
|
||||
|
||||
// backgroundFlush 后台协程,定期刷新缓冲区
|
||||
@@ -202,14 +216,10 @@ func (a *AuditLogger) backgroundFlush() {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
a.mu.Lock()
|
||||
_ = a.flush()
|
||||
a.mu.Unlock()
|
||||
_ = a.flushSynced()
|
||||
case <-a.stopChan:
|
||||
// 停止前刷新一次
|
||||
a.mu.Lock()
|
||||
_ = a.flush()
|
||||
a.mu.Unlock()
|
||||
_ = a.flushSynced()
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -227,31 +237,54 @@ func (a *AuditLogger) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 刷新剩余缓冲区
|
||||
if err := a.flush(); err != nil {
|
||||
// 刷新剩余缓冲区(进程退出路径,Sync 在锁内不影响运行期热路径)
|
||||
if f, err := a.flush(); err != nil {
|
||||
return err
|
||||
} else if f != nil {
|
||||
if err := f.Sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭文件
|
||||
return a.logFile.Close()
|
||||
}
|
||||
|
||||
// GetRecentLogs 获取最近的审计日志
|
||||
// GetRecentLogs 获取最近的审计日志(尾部倒读:文件大于 64KB 时只 Seek 读末尾 64KB,
|
||||
// 日志单行远小于 64KB,足以覆盖 limit 条最新记录,避免随日志增长全量读盘)
|
||||
func GetRecentLogs(logDir string, limit int) ([]AuditLogEntry, error) {
|
||||
// 读取今天的日志文件
|
||||
timestamp := time.Now().Format("2006-01-02")
|
||||
logPath := filepath.Join(logDir, fmt.Sprintf("audit_%s.log", timestamp))
|
||||
|
||||
data, err := os.ReadFile(logPath)
|
||||
f, err := os.Open(logPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// 解析JSON(每行一个条目)
|
||||
var entries []AuditLogEntry
|
||||
const tailSize = 64 * 1024
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
offset := info.Size() - tailSize
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
data := make([]byte, info.Size()-offset)
|
||||
if _, err := f.ReadAt(data, offset); err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 从文件中间开始读时首个字节位置未必在行边界,丢弃首条残行
|
||||
lines := parseLines(string(data))
|
||||
if offset > 0 && len(lines) > 0 {
|
||||
lines = lines[1:]
|
||||
}
|
||||
|
||||
// 从后往前读取(最新的在前)
|
||||
// 解析JSON(每行一个条目),从后往前读取(最新的在前)
|
||||
var entries []AuditLogEntry
|
||||
start := len(lines) - limit
|
||||
if start < 0 {
|
||||
start = 0
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"u-desk/internal/common"
|
||||
)
|
||||
|
||||
// ========== 辅助函数 ==========
|
||||
@@ -43,10 +45,10 @@ func OpenPath(path string) error {
|
||||
}
|
||||
|
||||
// 给进程一点时间启动
|
||||
go func() {
|
||||
common.Go("fs-process-release", func() {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
cmd.Process.Release()
|
||||
}()
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func NewRecycleBin(binPath string) (*RecycleBin, error) {
|
||||
}
|
||||
|
||||
// 启动自动清理协程
|
||||
go bin.autoCleanup()
|
||||
common.Go("recycle-autoclean", bin.autoCleanup)
|
||||
|
||||
return bin, nil
|
||||
}
|
||||
|
||||
@@ -878,10 +878,15 @@ func (s *FileSystemService) ResolveShortcut(lnkPath string) (targetPath string,
|
||||
lnkPath,
|
||||
)
|
||||
|
||||
// 执行 PowerShell 命令
|
||||
cmd := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", psScript)
|
||||
// 执行 PowerShell 命令(10s 超时:PowerShell 冷启动/被安全软件拖住时不无限挂起)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, "powershell", "-NoProfile", "-NonInteractive", "-Command", psScript)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return "", fmt.Errorf("解析快捷方式超时(10s)")
|
||||
}
|
||||
return "", fmt.Errorf("解析快捷方式失败: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"u-desk/internal/common"
|
||||
)
|
||||
|
||||
// ZipFileEntry 表示 zip 文件中的一个文件条目
|
||||
@@ -231,7 +233,7 @@ func ExtractFileFromZipToTemp(zipPath, filePath string) (string, error) {
|
||||
debugLog("[ExtractFileFromZipToTemp] 开始提取: %s from %s", filePath, zipPath)
|
||||
|
||||
// 启动临时文件清理协程
|
||||
go CleanOldTempFiles()
|
||||
common.Go("zip-clean-temp", CleanOldTempFiles)
|
||||
|
||||
// 创建临时目录
|
||||
tempDir := filepath.Join(os.TempDir(), TempFileDir)
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"u-desk/internal/common"
|
||||
)
|
||||
|
||||
type Watcher struct {
|
||||
@@ -62,7 +63,7 @@ func (w *Watcher) WatchFile(path string) error {
|
||||
w.watched = absPath
|
||||
|
||||
// 后台消费事件
|
||||
go w.consumeEvents()
|
||||
common.Go("filewatch-consume", w.consumeEvents)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -210,10 +210,10 @@ func InstallUpdateWithHash(installerPath string, autoRestart bool, expectedHash
|
||||
|
||||
// 自动重启
|
||||
if autoRestart {
|
||||
go func() {
|
||||
common.Go("update-restart", func() {
|
||||
time.Sleep(2 * time.Second)
|
||||
restartApplication()
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
return &InstallResult{Success: true, Message: "安装成功"}, nil
|
||||
@@ -275,10 +275,10 @@ func replaceExecutableFileWindows(newFilePath, execPath string) error {
|
||||
}
|
||||
|
||||
// 延迟删除旧文件
|
||||
go func() {
|
||||
common.Go("update-remove-old-exe", func() {
|
||||
time.Sleep(10 * time.Second)
|
||||
os.Remove(oldExecPath)
|
||||
}()
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+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