fix(backend): Fix error logs for normal exits of SSH/database connections

This commit is contained in:
pycook
2025-08-11 21:43:24 +08:00
parent cad28d1417
commit e9cd7d9807
6 changed files with 68 additions and 24 deletions

View File

@@ -13,6 +13,7 @@ import (
"github.com/creack/pty"
"go.uber.org/zap"
"github.com/veops/oneterm/internal/connector/protocols"
"github.com/veops/oneterm/internal/model"
gsession "github.com/veops/oneterm/internal/session"
"github.com/veops/oneterm/internal/tunneling"
@@ -55,12 +56,6 @@ func connectDB(sess *gsession.Session, asset *model.Asset, account *model.Accoun
return fmt.Errorf("unsupported protocol: %s", sess.Protocol)
}
logger.L().Info("Starting database client",
zap.String("command", clientConfig.Command),
zap.Strings("args", clientConfig.Args),
zap.String("host", ip),
zap.Int("port", port))
// Create command and pseudo-terminal
cmd := exec.CommandContext(sess.Gctx, clientConfig.Command, clientConfig.Args...)
cmd.Env = append(os.Environ(), "TERM=xterm-256color")
@@ -88,7 +83,13 @@ func connectDB(sess *gsession.Session, asset *model.Asset, account *model.Accoun
// Monitor process exit
sess.G.Go(func() error {
err := cmd.Wait()
logger.L().Info("Database client process exited", zap.Error(err), zap.String("protocol", protocol))
// Log process exit - only log as error if there was an actual error
if err != nil {
logger.L().Error("Database client process exited with error", zap.Error(err), zap.String("protocol", protocol))
} else {
logger.L().Info("Database client process exited normally", zap.String("protocol", protocol))
}
// Only send termination message if not already sent
if atomic.CompareAndSwapInt32(&exitMessageSent, 0, 1) {
@@ -98,10 +99,16 @@ func connectDB(sess *gsession.Session, asset *model.Asset, account *model.Accoun
}
sess.Once.Do(func() {
logger.L().Info("Closing AwayChan from database client monitor")
logger.L().Debug("Closing AwayChan from database client monitor")
close(chs.AwayChan)
})
return fmt.Errorf("database client process terminated: %w", err)
// Return appropriate error
if err != nil {
return fmt.Errorf("database client process terminated with error: %w", err)
}
// Return nil for normal exit - this is not an error condition
return nil
})
// Goroutine 1: Process input, detect exit command
@@ -213,7 +220,8 @@ func connectDB(sess *gsession.Session, asset *model.Asset, account *model.Accoun
case <-sess.Gctx.Done():
return nil
case <-chs.AwayChan:
return fmt.Errorf("away")
// Normal termination - return sentinel error
return protocols.ErrSessionClosed
case window := <-chs.WindowChan:
// Adjust terminal size
_ = pty.Setsize(ptmx, &pty.Winsize{

View File

@@ -93,7 +93,8 @@ func ConnectGuacd(ctx *gin.Context, sess *gsession.Session, asset *model.Asset,
case <-sess.Gctx.Done():
return nil
case <-chs.AwayChan:
return fmt.Errorf("away")
// Normal termination - return sentinel error
return ErrSessionClosed
case in := <-chs.InChan:
t.Write(in)
}

View File

@@ -80,7 +80,12 @@ func ConnectSsh(ctx *gin.Context, sess *gsession.Session, asset *model.Asset, ac
sess.G.Go(func() error {
err = sshSess.Wait()
return fmt.Errorf("ssh session wait end %w", err)
// Always close AwayChan when SSH session ends
sess.Once.Do(func() { close(chs.AwayChan) })
if err != nil {
return fmt.Errorf("ssh session wait end with error: %w", err)
}
return nil
})
chs.ErrChan <- err
@@ -114,7 +119,8 @@ func ConnectSsh(ctx *gin.Context, sess *gsession.Session, asset *model.Asset, ac
case <-sess.Gctx.Done():
return nil
case <-chs.AwayChan:
return fmt.Errorf("away")
// Normal termination - return sentinel error
return ErrSessionClosed
case window := <-chs.WindowChan:
if err := sshSess.WindowChange(window.Height, window.Width); err != nil {
logger.L().Warn("reset window size failed", zap.Error(err))

View File

@@ -1,6 +1,7 @@
package protocols
import (
"errors"
"fmt"
"net/http"
"strings"
@@ -23,6 +24,10 @@ import (
)
var (
// ErrSessionClosed is a sentinel error for normal session termination
// This is returned when a session is closed normally (e.g., user exits)
ErrSessionClosed = errors.New("session closed normally")
Upgrader = websocket.Upgrader{
HandshakeTimeout: time.Minute,
ReadBufferSize: 4096,

View File

@@ -335,6 +335,10 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
// Let the table handle all navigation keys
m.table, cmd = m.table.Update(msg)
return m, cmd
default:
// For any other key messages, let the table handle them
m.table, cmd = m.table.Update(msg)
return m, cmd
}
case tea.WindowSizeMsg:
@@ -358,8 +362,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
newHeight = maxHeight
}
m.table.SetHeight(newHeight)
default:
// Update table for other messages
// Let table also handle the window size message
m.table, cmd = m.table.Update(msg)
}

View File

@@ -5,6 +5,8 @@ import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"sort"
"strings"
"time"
@@ -543,18 +545,32 @@ func (m *view) handleConnectionCommand(cmd string) tea.Cmd {
// Setup connection parameters
pty, _, _ := m.Sess.Pty()
m.Ctx.Request.URL.RawQuery = fmt.Sprintf("w=%d&h=%d", pty.Window.Width, pty.Window.Height)
m.Ctx.Params = nil
m.Ctx.Params = append(m.Ctx.Params, gin.Param{Key: "account_id", Value: cast.ToString(m.combines[cmd][0])})
m.Ctx.Params = append(m.Ctx.Params, gin.Param{Key: "asset_id", Value: cast.ToString(m.combines[cmd][1])})
m.Ctx.Params = append(m.Ctx.Params, gin.Param{Key: "protocol", Value: fmt.Sprintf("%s:%d", p, m.combines[cmd][2])})
m.Ctx = m.Ctx.Copy()
m.Ctx.Set("sessionType", model.SESSIONTYPE_CLIENT)
// Create a copy of the context first to avoid modifying the original
newCtx := m.Ctx.Copy()
// Ensure Request and URL are properly initialized
if newCtx.Request == nil {
newCtx.Request = &http.Request{
RemoteAddr: m.Sess.RemoteAddr().String(),
URL: &url.URL{},
}
}
if newCtx.Request.URL == nil {
newCtx.Request.URL = &url.URL{}
}
newCtx.Request.URL.RawQuery = fmt.Sprintf("w=%d&h=%d", pty.Window.Width, pty.Window.Height)
newCtx.Params = nil
newCtx.Params = append(newCtx.Params, gin.Param{Key: "account_id", Value: cast.ToString(m.combines[cmd][0])})
newCtx.Params = append(newCtx.Params, gin.Param{Key: "asset_id", Value: cast.ToString(m.combines[cmd][1])})
newCtx.Params = append(newCtx.Params, gin.Param{Key: "protocol", Value: fmt.Sprintf("%s:%d", p, m.combines[cmd][2])})
newCtx.Set("sessionType", model.SESSIONTYPE_CLIENT)
m.connecting = true
return tea.Sequence(
tea.Printf("🔌 Establishing connection to %s...\n", cmd),
tea.Exec(&connector{Ctx: m.Ctx, Sess: m.Sess, Vw: m, gctx: m.gctx}, func(err error) tea.Msg {
tea.Exec(&connector{Ctx: newCtx, Sess: m.Sess, Vw: m, gctx: m.gctx}, func(err error) tea.Msg {
m.connecting = false
if err != nil {
return errMsg(fmt.Errorf("❌ Connection failed: %v", err))
@@ -864,7 +880,12 @@ func (conn *connector) Run() error {
myConnector.HandleTerm(gsess, nil)
if err = gsess.G.Wait(); err != nil {
logger.L().Error("sshsrv run stopped", zap.String("sessionId", gsess.SessionId), zap.Error(err))
// Check if this is the normal termination sentinel error
if err.Error() == "session closed normally" {
logger.L().Debug("sshsrv session ended normally", zap.String("sessionId", gsess.SessionId))
} else {
logger.L().Debug("sshsrv run stopped", zap.String("sessionId", gsess.SessionId), zap.Error(err))
}
}
conn.stdout.Write([]byte("\n\n"))