feat: «Здесь» from a machine's file pane opens the terminal in that pane's folder (the agent starts the shell there; an older agent, or a folder gone, at home)
This commit is contained in:
1 parent
4fb26a9652
commit
57c2f1338f
10 files changed
+143
-12
No files matched your search
@@ -330,7 +330,7 @@ func (a *Agent) pollOnce(ctx, reqCtx context.Context) error {
|
||||
func (a *Agent) dispatch(ctx context.Context, msg proto.ControlMessage) {
|
||||
switch msg.Type {
|
||||
case proto.CtrlOpenTerminal:
|
||||
go a.runTerminal(ctx, msg.SessionID, msg.User)
|
||||
go a.runTerminal(ctx, msg.SessionID, msg.User, msg.Dir)
|
||||
case proto.CtrlOpenForward:
|
||||
go a.runForward(ctx, msg.SessionID, msg.Port)
|
||||
case proto.CtrlTask:
|
||||
|
||||
@@ -16,8 +16,8 @@ import (
|
||||
|
||||
// runTerminal spawns a PTY and bridges it to the session.
|
||||
// user is the OS user to run the shell as ("" = the agent's own user).
|
||||
func (a *Agent) runTerminal(parent context.Context, sid, user string) {
|
||||
term, err := StartPTY(user)
|
||||
func (a *Agent) runTerminal(parent context.Context, sid, user, dir string) {
|
||||
term, err := StartPTY(user, dir)
|
||||
if err != nil {
|
||||
log.Printf("term %s: start pty: %v", sid, err)
|
||||
// Show the reason in the browser terminal before closing it.
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -41,8 +43,12 @@ func CloseTerminals() {
|
||||
// StartPTY launches a login shell attached to a new PTY. With user empty (or
|
||||
// equal to the agent's own user) it is the agent's shell - root by default,
|
||||
// since the agent runs as a root systemd service. Any other user is entered via
|
||||
// `su -l`, which only a root agent can do without a password.
|
||||
func StartPTY(user string) (*PTY, error) {
|
||||
// `su -l`, which only a root agent can do without a password. dir, when it
|
||||
// is a folder, is where the shell starts instead of home.
|
||||
func StartPTY(user, dir string) (*PTY, error) {
|
||||
if dir != "" && (!filepath.IsAbs(dir) || strings.ContainsAny(dir, "\x00\r\n") || !isDir(dir)) {
|
||||
dir = ""
|
||||
}
|
||||
var cmd *exec.Cmd
|
||||
env := append(os.Environ(), "TERM=xterm-256color")
|
||||
if user == "" || user == currentUser() {
|
||||
@@ -54,6 +60,9 @@ func StartPTY(user string) (*PTY, error) {
|
||||
cmd.Dir = home
|
||||
env = append(env, "HOME="+home)
|
||||
}
|
||||
if dir != "" {
|
||||
cmd.Dir = dir
|
||||
}
|
||||
} else {
|
||||
if !validUserName(user) {
|
||||
return nil, fmt.Errorf("недопустимое имя пользователя %q", user)
|
||||
@@ -69,6 +78,11 @@ func StartPTY(user string) (*PTY, error) {
|
||||
return nil, fmt.Errorf("su не найден: %w", err)
|
||||
}
|
||||
cmd = exec.Command(su, "-l", user)
|
||||
// su -l always goes home; to start elsewhere, its shell changes
|
||||
// directory first and becomes the login shell (sh and csh alike).
|
||||
if dir != "" {
|
||||
cmd = exec.Command(su, "-l", user, "-c", "cd "+shQuote(dir)+"; exec \"$SHELL\" -l")
|
||||
}
|
||||
// su -l goes to the user's home; starting there too keeps the shell
|
||||
// out of "/" should it not.
|
||||
if home := homeDir(user); isDir(home) {
|
||||
@@ -126,3 +140,9 @@ func (p *PTY) Close() error {
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// shQuote puts s in single quotes for sh and csh alike, closing and
|
||||
// reopening them around each quote inside.
|
||||
func shQuote(s string) string {
|
||||
return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// A terminal opened with a folder starts its shell there; a folder that is
|
||||
// not there leaves it at home.
|
||||
func TestStartPTYDir(t *testing.T) {
|
||||
dir, err := filepath.EvalSymlinks(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sub := filepath.Join(dir, "sub dir")
|
||||
if err := os.Mkdir(sub, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := ptyPwd(t, sub); got != sub {
|
||||
t.Fatalf("pwd = %q, want %q", got, sub)
|
||||
}
|
||||
home, _ := filepath.EvalSymlinks(homeDir(""))
|
||||
if got := ptyPwd(t, filepath.Join(dir, "missing")); got != home {
|
||||
t.Fatalf("pwd for a missing folder = %q, want home %q", got, home)
|
||||
}
|
||||
}
|
||||
|
||||
// ptyPwd opens a terminal at dir and returns where its shell is.
|
||||
func ptyPwd(t *testing.T, dir string) string {
|
||||
t.Helper()
|
||||
t.Setenv("SHELL", "/bin/sh")
|
||||
p, err := StartPTY("", dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer p.Close()
|
||||
if _, err := p.Write([]byte("echo \"@@$(pwd -P)@@\"\n")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var out strings.Builder
|
||||
buf := make([]byte, 4096)
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
n, err := p.Read(buf)
|
||||
out.Write(buf[:n])
|
||||
s := out.String()
|
||||
// The echoed command line has "@@$(pwd" in it; the answer does not.
|
||||
for _, part := range strings.Split(s, "@@") {
|
||||
if strings.HasPrefix(part, "/") && strings.Contains(s, part+"@@") {
|
||||
return part
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
t.Fatalf("no pwd in %q", out.String())
|
||||
return ""
|
||||
}
|
||||
Reference in new issue
Block a user