feat: «Здесь» from a machine's terminal opens the file pane in the terminal's folder (asked of the agent); Аккаунт › Вид can turn «Здесь» back to the home folder. Version 0.2.1
This commit is contained in:
1 parent
57c2f1338f
commit
087eabf3b4
12 files changed
+163
-6
No files matched your search
@@ -26,6 +26,8 @@ func (a *Agent) runTerminal(parent context.Context, sid, user, dir string) {
|
||||
return
|
||||
}
|
||||
log.Printf("term %s: shell started (user %q)", sid, user)
|
||||
termsBySID.Store(sid, term)
|
||||
defer termsBySID.Delete(sid)
|
||||
a.bridgeStream(parent, sid, term, term.Resize)
|
||||
log.Printf("term %s: ended", sid)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// termsBySID are the running terminals by their session, so the panel can
|
||||
// ask where one of them is.
|
||||
var termsBySID sync.Map // sid -> *PTY
|
||||
|
||||
// Cwd is the folder the terminal is in now: that of the program in front
|
||||
// (the shell, or what it runs), else of the shell itself.
|
||||
func (p *PTY) Cwd() (string, error) {
|
||||
pid := 0
|
||||
if g, err := unix.IoctlGetInt(int(p.f.Fd()), unix.TIOCGPGRP); err == nil && g > 0 {
|
||||
pid = g
|
||||
}
|
||||
if pid > 0 {
|
||||
if d, err := procCwd(pid); err == nil {
|
||||
return d, nil
|
||||
}
|
||||
}
|
||||
return procCwd(p.cmd.Process.Pid)
|
||||
}
|
||||
|
||||
// procCwd is a process's working folder: /proc on Linux, lsof elsewhere.
|
||||
func procCwd(pid int) (string, error) {
|
||||
if runtime.GOOS == "linux" {
|
||||
return os.Readlink("/proc/" + strconv.Itoa(pid) + "/cwd")
|
||||
}
|
||||
out, err := exec.Command("lsof", "-a", "-p", strconv.Itoa(pid), "-d", "cwd", "-Fn").Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, l := range strings.Split(string(out), "\n") {
|
||||
if strings.HasPrefix(l, "n/") {
|
||||
return l[1:], nil
|
||||
}
|
||||
}
|
||||
return "", errors.New("папка терминала не найдена")
|
||||
}
|
||||
|
||||
// termCwd answers the panel: where the terminal of session sid is now.
|
||||
func termCwd(sid string) (any, error) {
|
||||
v, ok := termsBySID.Load(sid)
|
||||
if !ok {
|
||||
return nil, errors.New("терминал не найден")
|
||||
}
|
||||
d, err := v.(*PTY).Cwd()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]string{"path": d}, nil
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/x-grk/longpolling-remote-access/internal/proto"
|
||||
)
|
||||
|
||||
// A terminal opened with a folder starts its shell there; a folder that is
|
||||
@@ -60,3 +63,53 @@ func ptyPwd(t *testing.T, dir string) string {
|
||||
t.Fatalf("no pwd in %q", out.String())
|
||||
return ""
|
||||
}
|
||||
|
||||
// A terminal tells the folder its shell is in now, after a cd.
|
||||
func TestPTYCwd(t *testing.T) {
|
||||
dir, err := filepath.EvalSymlinks(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sub := filepath.Join(dir, "next one")
|
||||
if err := os.Mkdir(sub, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("SHELL", "/bin/sh")
|
||||
p, err := StartPTY("", dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer p.Close()
|
||||
go func() { _, _ = io.Copy(io.Discard, p) }()
|
||||
if _, err := p.Write([]byte("cd 'next one'\n")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var got string
|
||||
for i := 0; i < 50; i++ {
|
||||
if got, err = p.Cwd(); err == nil && got == sub {
|
||||
return
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("Cwd = %q, %v; want %q", got, err, sub)
|
||||
}
|
||||
|
||||
// The panel asks for a terminal's folder by its session.
|
||||
func TestTermCwdTask(t *testing.T) {
|
||||
dir, _ := filepath.EvalSymlinks(t.TempDir())
|
||||
t.Setenv("SHELL", "/bin/sh")
|
||||
p, err := StartPTY("", dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer p.Close()
|
||||
termsBySID.Store("s1", p)
|
||||
defer termsBySID.Delete("s1")
|
||||
res, err := dispatchTask(proto.TaskEnvelope{Type: proto.TaskTermCwd, Args: []byte(`{"session":"s1"}`)})
|
||||
if err != nil || res.(map[string]string)["path"] != dir {
|
||||
t.Fatalf("term.cwd = %v, %v; want %q", res, err, dir)
|
||||
}
|
||||
if _, err := dispatchTask(proto.TaskEnvelope{Type: proto.TaskTermCwd, Args: []byte(`{"session":"nope"}`)}); err == nil {
|
||||
t.Fatal("an unknown session has a folder")
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,10 @@ func dispatchTask(env proto.TaskEnvelope) (any, error) {
|
||||
return nil, addUser(a)
|
||||
case proto.TaskDBTables, proto.TaskDBRows, proto.TaskDBSet, proto.TaskDBAdd, proto.TaskDBDel, proto.TaskDBSQL:
|
||||
return dbTask(env)
|
||||
case proto.TaskTermCwd:
|
||||
var a proto.TermCwdArgs
|
||||
_ = json.Unmarshal(env.Args, &a)
|
||||
return termCwd(a.Session)
|
||||
}
|
||||
return dispatchFS(env)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user