mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/1Panel-dev/1Panel.git
synced 2026-09-20 08:03:55 +08:00
* feat(terminal): keep ssh sessions alive server-side with reattach Split the terminal ws handling into a Session (pty + ssh backend) and an Attachment (one websocket). A session outlives its websocket: a clean close (1000) ends the pty, any other disconnect keeps it for a 30-minute grace period and it can be reattached via `?session=<id>`. Output goes through a fixed 128KB ring buffer so a reattaching client gets the recent tail, with a truncation marker if it fell behind. Sessions are owner-scoped; a second attachment kicks the first (4409), unknown ids get 4404. New endpoints under /hosts/terminal/sessions (search, close) let the frontend list and recover sessions after a tab or browser is closed. * feat(terminal): floating terminal dock with session recovery Terminals now live in a layout-level host and are teleported into whichever view shows them, so leaving the terminal page no longer kills them. A dock handle on the right edge opens a non-modal dialog from any page with every live session, a picker for local shell / ssh hosts, minimize, and close-all. On page load the store recovers sessions the server still holds, so an accidentally closed tab or browser can resume within the grace period. The menu-tab label shows the live session count. * fix(terminal): page re-claims its slots under a locked menu tab With the terminal menu tab locked (keep-alive), leaving the page deactivates it instead of unmounting it, so the slot ref callback never re-runs on return. After the dock had taken the Terminal over and released it, nobody claimed it for the page again and it stayed parked in the hidden host. Claim/release slots explicitly on mount, activated, deactivated and unmount, the same ownership rule the dock uses, instead of relying on the ref callback. * fix(terminal): logout closes every kept-alive terminal session A logged-out panel has nobody watching it, so nothing it left running should survive: core now tells the local agent to close all terminal sessions when the user logs out, changes the password, or changes the bind domain. Until now the teardown relied on the logging-out tab sending close code 1000; a second tab or a websocket held outside the SPA kept its shell after logout. Agent: terminal.CloseAll and POST /hosts/terminal/sessions/closeAll. Core: LogOut / deleteCurrentSession / BindDomain call it via proxy_local, best effort. * fix(terminal): pin a local shell to the node it was opened on The node a local shell connects to was resolved from the current node every time the websocket was built, so after switching nodes a reconnect carried the old session id to the new node (4404) and then opened a shell there instead. Store the operateNode on the entry when it is created; ssh shells keep going to the master. Shells on a non-master node get the node name in their title so a restore in another node's view can tell them apart.
86 lines
2.1 KiB
Go
86 lines
2.1 KiB
Go
package terminal
|
|
|
|
import (
|
|
"io"
|
|
"time"
|
|
|
|
gossh "golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
// sshBackend drives one interactive shell and owns its ssh client.
|
|
type sshBackend struct {
|
|
client *gossh.Client
|
|
session *gossh.Session
|
|
stdin io.WriteCloser
|
|
}
|
|
|
|
// newSSHBackend opens a shell with a pty on client and streams its output to out.
|
|
func newSSHBackend(client *gossh.Client, cols, rows int, initCmd string, out io.Writer) (*sshBackend, error) {
|
|
sshSession, err := client.NewSession()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stdinPipe, err := sshSession.StdinPipe()
|
|
if err != nil {
|
|
_ = sshSession.Close()
|
|
return nil, err
|
|
}
|
|
sshSession.Stdout = out
|
|
sshSession.Stderr = out
|
|
|
|
modes := gossh.TerminalModes{
|
|
gossh.ECHO: 1,
|
|
gossh.TTY_OP_ISPEED: 14400,
|
|
gossh.TTY_OP_OSPEED: 14400,
|
|
}
|
|
if err := sshSession.RequestPty("xterm", rows, cols, modes); err != nil {
|
|
_ = sshSession.Close()
|
|
return nil, err
|
|
}
|
|
if err := sshSession.Shell(); err != nil {
|
|
_ = sshSession.Close()
|
|
return nil, err
|
|
}
|
|
if len(initCmd) != 0 {
|
|
time.Sleep(100 * time.Millisecond)
|
|
_, _ = stdinPipe.Write([]byte(initCmd + "\n"))
|
|
}
|
|
return &sshBackend{client: client, session: sshSession, stdin: stdinPipe}, nil
|
|
}
|
|
|
|
// Write forwards p to the shell stdin.
|
|
func (b *sshBackend) Write(p []byte) (int, error) {
|
|
return b.stdin.Write(p)
|
|
}
|
|
|
|
// Resize changes the pty window size.
|
|
func (b *sshBackend) Resize(cols, rows int) error {
|
|
return b.session.WindowChange(rows, cols)
|
|
}
|
|
|
|
// Wait blocks until the remote shell exits.
|
|
func (b *sshBackend) Wait() error {
|
|
return b.session.Wait()
|
|
}
|
|
|
|
// Keepalive probes the ssh connection; callers should bound it.
|
|
func (b *sshBackend) Keepalive() error {
|
|
if b.client == nil {
|
|
return nil
|
|
}
|
|
// a failure reply with no error still proves the connection is alive
|
|
_, _, err := b.client.SendRequest("keepalive@openssh.com", true, nil)
|
|
return err
|
|
}
|
|
|
|
// Close terminates the ssh session and the ssh client owned by this backend.
|
|
func (b *sshBackend) Close() error {
|
|
err := b.session.Close()
|
|
if b.client != nil {
|
|
if clientErr := b.client.Close(); err == nil {
|
|
err = clientErr
|
|
}
|
|
}
|
|
return err
|
|
}
|