Compare commits

..

2 Commits

Author SHA1 Message Date
hyung-hwan 351670f311 updated the ssh session broker 2026-06-05 22:50:08 +09:00
hyung-hwan 24d26764a7 removed unneeded info from the ssh session terminal 2026-06-05 20:42:17 +09:00
2 changed files with 85 additions and 23 deletions
+76 -19
View File
@@ -2503,6 +2503,8 @@ event_loop:
var attachedItem models.SSHSession var attachedItem models.SSHSession
var attachedSessionID string var attachedSessionID string
var attachedPrepared *sshSessionPrepared var attachedPrepared *sshSessionPrepared
var attachedStatus string
var attachedMessage string
if strings.TrimSpace(msg.SessionID) == "" { if strings.TrimSpace(msg.SessionID) == "" {
api.sendSSHSessionWS(&sendMu, ws, sshSessionStreamMessage{ api.sendSSHSessionWS(&sendMu, ws, sshSessionStreamMessage{
@@ -2539,6 +2541,40 @@ event_loop:
}) })
continue continue
} }
if !preparedOK {
attachedStatus = "closed"
attachedMessage = item.Error
if item.Status == "error" {
attachedStatus = "error"
}
if attachedMessage == "" {
if item.Status == "closed" {
attachedMessage = "SSH session is closed"
} else if item.Status == "error" {
attachedMessage = "SSH session failed"
} else {
attachedMessage = "SSH session cannot be resumed. Connect again to start a new session."
}
}
api.sendSSHSessionWS(&sendMu, ws, sshSessionStreamMessage{
SessionID: msg.SessionID,
Type: "status",
Status: attachedStatus,
Message: attachedMessage,
})
continue
}
prepared, preparedOK = api.SSHPreparedSessionStore.Take(msg.SessionID)
if !preparedOK {
api.sendSSHSessionWS(&sendMu, ws, sshSessionStreamMessage{
SessionID: msg.SessionID,
Type: "status",
Status: "error",
Message: "ssh session preparation not found",
})
continue
}
attachedPrepared = &prepared
attachment = &sshWorkspaceAttachment{ attachment = &sshWorkspaceAttachment{
InputCh: make(chan sshSessionStreamMessage, sshSessionInputBufferSize), InputCh: make(chan sshSessionStreamMessage, sshSessionInputBufferSize),
InputErrCh: make(chan error, 1), InputErrCh: make(chan error, 1),
@@ -2551,12 +2587,6 @@ event_loop:
user.Username) user.Username)
attachedItem = item attachedItem = item
attachedSessionID = attachedItem.ID attachedSessionID = attachedItem.ID
if preparedOK {
prepared, preparedOK = api.SSHPreparedSessionStore.Take(msg.SessionID)
if preparedOK {
attachedPrepared = &prepared
}
}
go func(runItem models.SSHSession, runPrepared *sshSessionPrepared, runSessionID string, runAttachment *sshWorkspaceAttachment) { go func(runItem models.SSHSession, runPrepared *sshSessionPrepared, runSessionID string, runAttachment *sshWorkspaceAttachment) {
defer func() { defer func() {
select { select {
@@ -2665,7 +2695,17 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
var transcriptRecorder *sshSessionTranscriptRecorder var transcriptRecorder *sshSessionTranscriptRecorder
var transcriptSendBinary func([]byte) var transcriptSendBinary func([]byte)
var promptedAuthInput SSHPromptedAuthInput var promptedAuthInput SSHPromptedAuthInput
var runtimeStartedAt time.Time
runtimeStartedAt = time.Now()
defer func() {
api.Logger.Write(logIDSSHBroker, codit_logger.LOG_INFO,
"session runtime exited session=%s requester=%s user=%s dur_ms=%d",
sessionItem.ID,
sessionItem.RemoteAddr,
user.Username,
time.Since(runtimeStartedAt).Milliseconds())
}()
runtimeSession = &sshActiveSession{} runtimeSession = &sshActiveSession{}
runtimeSession.SetResources(transportWS, nil, nil, nil) runtimeSession.SetResources(transportWS, nil, nil, nil)
if api.SSHSessionRegistry != nil { if api.SSHSessionRegistry != nil {
@@ -2727,7 +2767,7 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", "", 0, now, closeReason) _ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", "", 0, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now) api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now)
if sendStatus != nil { if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
} }
return return
@@ -2808,7 +2848,7 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason) _ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now) api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now)
if sendStatus != nil { if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
} }
return return
@@ -2820,7 +2860,7 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason) _ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now) api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now)
if sendStatus != nil { if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
} }
return return
@@ -2840,7 +2880,7 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason) _ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now) api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now)
if sendStatus != nil { if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
} }
return return
@@ -2852,7 +2892,7 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason) _ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now) api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now)
if sendStatus != nil { if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
} }
return return
@@ -2872,7 +2912,7 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason) _ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now) api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now)
if sendStatus != nil { if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
} }
return return
@@ -2892,7 +2932,7 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason) _ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now) api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now)
if sendStatus != nil { if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
} }
return return
@@ -2910,7 +2950,7 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
if closeReason != "" { if closeReason != "" {
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason) _ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason)
if sendStatus != nil { if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
} }
return return
@@ -2926,7 +2966,7 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason) _ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now) api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now)
if sendStatus != nil { if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
} }
return return
@@ -2944,7 +2984,7 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason) _ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now) api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now)
if sendStatus != nil { if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
} }
return return
@@ -2956,7 +2996,7 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason) _ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now) api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now)
if sendStatus != nil { if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
} }
return return
@@ -2974,7 +3014,7 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason) _ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now) api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now)
if sendStatus != nil { if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
} }
return return
@@ -3055,7 +3095,7 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
if closeReason != "" { if closeReason != "" {
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, connectedAt, now, closeReason) _ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, connectedAt, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, connectedAt, now) api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, connectedAt, now)
if sendStatus != nil { if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
} }
return return
@@ -3340,6 +3380,23 @@ func sshSessionWebsocketCloseReason(err error) string {
return "websocket closed" return "websocket closed"
} }
func isSSHSessionTransportCloseReason(reason string) bool {
reason = strings.TrimSpace(reason)
if reason == "websocket closed" {
return true
}
if reason == "websocket send failed" {
return true
}
if reason == "websocket input queue full" {
return true
}
if reason == "detached" {
return true
}
return false
}
func (api *API) sendSSHSessionWS(mu *sync.Mutex, ws *websocket.Conn, msg sshSessionStreamMessage) error { func (api *API) sendSSHSessionWS(mu *sync.Mutex, ws *websocket.Conn, msg sshSessionStreamMessage) error {
var err error var err error
+9 -4
View File
@@ -332,6 +332,9 @@ function SessionTerminalPanel(props: SessionPanelProps) {
if (disposed || !term) return if (disposed || !term) return
streamActiveRef.current = isSSHSessionStreamActive(nextStatus) streamActiveRef.current = isSSHSessionStreamActive(nextStatus)
setStatus(nextStatus) setStatus(nextStatus)
if (!streamActiveRef.current) {
unregisterStream(sessionID)
}
if (message.status === 'connected') { if (message.status === 'connected') {
setSession((prev) => (prev ? { ...prev, host_key_fingerprint: message.message || prev.host_key_fingerprint, status: 'connected' } : prev)) setSession((prev) => (prev ? { ...prev, host_key_fingerprint: message.message || prev.host_key_fingerprint, status: 'connected' } : prev))
scheduleResize() scheduleResize()
@@ -350,6 +353,7 @@ function SessionTerminalPanel(props: SessionPanelProps) {
onTransportClosed: (message: string) => { onTransportClosed: (message: string) => {
if (disposed) return if (disposed) return
streamActiveRef.current = false streamActiveRef.current = false
unregisterStream(sessionID)
setStatus((prev) => (prev === 'closed' || prev === 'error' ? prev : 'closed')) setStatus((prev) => (prev === 'closed' || prev === 'error' ? prev : 'closed'))
if (message) setError((prev) => prev || message) if (message) setError((prev) => prev || message)
} }
@@ -584,13 +588,13 @@ function SessionTerminalPanel(props: SessionPanelProps) {
}} }}
> >
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1, flexWrap: 'wrap', px: 0.25 }}> <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1, flexWrap: 'wrap', px: 0.25 }}>
<Box sx={{ minWidth: 0, display: 'grid', gap: 0.25, flex: '1 1 320px' }}> <Box sx={{ minWidth: 0, display: 'grid', flex: '1 1 320px' }}>
<Typography variant="subtitle2" sx={{ minWidth: 0, wordBreak: 'break-word' }}> <Typography variant="subtitle2" >
{sessionTitle} {sessionTitle}
</Typography> </Typography>
{session ? ( {session ? (
<Typography variant="caption" color="text.secondary" sx={{ minWidth: 0, wordBreak: 'break-word' }}> <Typography variant="caption" color="text.secondary">
Host key: {session.host_key_fingerprint || '-'} · Auth: {session.auth_method} · User: {session.username} Host key: {session.host_key_fingerprint || '-'}
</Typography> </Typography>
) : null} ) : null}
</Box> </Box>
@@ -1079,6 +1083,7 @@ export default function SSHSessionPage() {
const unregisterStream = useCallback((sessionID: string) => { const unregisterStream = useCallback((sessionID: string) => {
delete streamHandlersRef.current[sessionID] delete streamHandlersRef.current[sessionID]
delete attachedSessionIDsRef.current[sessionID]
}, []) }, [])
const sendStreamInput = useCallback((sessionID: string, data: string) => { const sendStreamInput = useCallback((sessionID: string, data: string) => {