9p: add proc/ directory for detached process inspection
- ls /s/{id}/proc/ lists detached PIDs
- cat /s/{id}/proc/{pid} reads ring buffer output
- rm /s/{id}/proc/{pid} dismisses the process
- echo detach > /s/{id}/ctl detaches running process
- echo "signal {pid} [TERM|KILL]" > /s/{id}/ctl signals detached process
Walk validates PID exists in detached list before succeeding.
This commit is contained in:
parent
61240e02de
commit
4ff3961dfd
|
|
@ -161,12 +161,15 @@ func (s *Server) dirMode(path string) plan9.Perm {
|
|||
func (s *Server) filePerm(path string) os.FileMode {
|
||||
// Session files have per-file overrides.
|
||||
if strings.HasPrefix(path, fs.PathSessions) {
|
||||
// Peer files are writable.
|
||||
// Peer/proc file permissions.
|
||||
name := strings.TrimPrefix(path, fs.PathSessions)
|
||||
parts := strings.SplitN(name, "/", 3)
|
||||
if len(parts) == 3 && parts[1] == "peer" {
|
||||
return 0666
|
||||
}
|
||||
if len(parts) == 3 && parts[1] == "proc" {
|
||||
return 0444
|
||||
}
|
||||
base := pathBase(path)
|
||||
if m, ok := fs.Perms[fs.PathSessionFile].Files[base]; ok {
|
||||
return m
|
||||
|
|
@ -503,9 +506,9 @@ func (s *Server) pathType(path string) string {
|
|||
}
|
||||
return "dir"
|
||||
}
|
||||
// /s/{id}/peer — directory
|
||||
// /s/{id}/peer or /s/{id}/proc — directory
|
||||
parts := strings.SplitN(name, "/", 3)
|
||||
if len(parts) == 2 && parts[1] == "peer" {
|
||||
if len(parts) == 2 && (parts[1] == "peer" || parts[1] == "proc") {
|
||||
return "dir"
|
||||
}
|
||||
// /s/{id}/peer/{peer-id} — verify peer exists
|
||||
|
|
@ -519,6 +522,19 @@ func (s *Server) pathType(path string) string {
|
|||
}
|
||||
return ""
|
||||
}
|
||||
// /s/{id}/proc/{pid} — verify PID exists in detached list
|
||||
if len(parts) == 3 && parts[1] == "proc" {
|
||||
sess := s.sessionMgr.Session(parts[0])
|
||||
if sess == nil {
|
||||
return ""
|
||||
}
|
||||
for _, p := range sess.Core.ListDetached() {
|
||||
if fmt.Sprintf("%d", p.PID) == parts[2] {
|
||||
return "file"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
// /s/{id}/{file} — file
|
||||
return "file"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
|
@ -265,8 +266,9 @@ func (s *Manager) Readdir(name string) ([]os.DirEntry, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Append peer/ directory
|
||||
// Append peer/ and proc/ directories
|
||||
entries = append(entries, fs.DirEntry("peer", 0755))
|
||||
entries = append(entries, fs.DirEntry("proc", 0755))
|
||||
return entries, nil
|
||||
}
|
||||
// {id}/peer — list peer session IDs
|
||||
|
|
@ -278,6 +280,15 @@ func (s *Manager) Readdir(name string) ([]os.DirEntry, error) {
|
|||
}
|
||||
return entries, nil
|
||||
}
|
||||
// {id}/proc — list detached process PIDs
|
||||
if len(parts) == 2 && parts[1] == "proc" {
|
||||
procs := sess.Core.ListDetached()
|
||||
entries := make([]os.DirEntry, len(procs))
|
||||
for i, p := range procs {
|
||||
entries[i] = fs.FileEntry(fmt.Sprintf("%d", p.PID), 0666)
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
return nil, fmt.Errorf("%s: not a directory", name)
|
||||
}
|
||||
|
||||
|
|
@ -408,6 +419,14 @@ func (s *Manager) stat(name string) (os.FileInfo, error) {
|
|||
}
|
||||
return &fs.SyntheticFileInfo{Name_: peerID, Mode_: 0666}, nil
|
||||
}
|
||||
// Proc directory: {id}/proc
|
||||
if parts[1] == "proc" {
|
||||
if len(parts) == 2 {
|
||||
return &fs.SyntheticFileInfo{Name_: "proc", Mode_: 0755, IsDir_: true}, nil
|
||||
}
|
||||
// Proc file: {id}/proc/{pid} — read-only output
|
||||
return &fs.SyntheticFileInfo{Name_: parts[2], Mode_: 0444}, nil
|
||||
}
|
||||
// Session file: {id}/{file}
|
||||
sfs, err := s.openStore(sess)
|
||||
if err != nil {
|
||||
|
|
@ -423,6 +442,35 @@ func (s *Manager) openEntry(name string) (fs.File, error) {
|
|||
|
||||
// Peer file: {id}/peer/{peer-id} — write submits prompt to peer
|
||||
parts := strings.SplitN(name, "/", 3)
|
||||
|
||||
// Proc file: {id}/proc/{pid} — read returns ring buffer output
|
||||
if len(parts) == 3 && parts[1] == "proc" {
|
||||
sessID := parts[0]
|
||||
sess := s.Session(sessID)
|
||||
if sess == nil {
|
||||
return nil, fmt.Errorf("%s: not found", name)
|
||||
}
|
||||
pidStr := parts[2]
|
||||
pid, err := strconv.Atoi(pidStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid pid: %s", pidStr)
|
||||
}
|
||||
return &fs.FileConfig{
|
||||
StatFn: func() (os.FileInfo, error) {
|
||||
return &fs.SyntheticFileInfo{Name_: pidStr, Mode_: 0444}, nil
|
||||
},
|
||||
ReadFn: func() ([]byte, error) {
|
||||
output, err := sess.Core.GetDetachedOutput(pid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(output), nil
|
||||
},
|
||||
WriteFn: func([]byte) error { return fmt.Errorf("read-only") },
|
||||
BlockingReadFn: notBlocking,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if len(parts) == 3 && parts[1] == "peer" {
|
||||
sessID := parts[0]
|
||||
peerID := parts[2]
|
||||
|
|
@ -521,6 +569,21 @@ func (s *Manager) del(name string) error {
|
|||
if len(parts) == 3 && parts[1] == "peer" {
|
||||
return s.PeerRemove(sessID, parts[2])
|
||||
}
|
||||
// Proc dismiss: {id}/proc/{pid}
|
||||
if len(parts) == 3 && parts[1] == "proc" {
|
||||
sess := s.Session(sessID)
|
||||
if sess == nil {
|
||||
return fmt.Errorf("session not found: %s", sessID)
|
||||
}
|
||||
pid, err := strconv.Atoi(parts[2])
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid pid: %s", parts[2])
|
||||
}
|
||||
if !sess.Core.DismissDetached(pid) {
|
||||
return fmt.Errorf("process %d not found or still running", pid)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// Session files are synthetic; allow rm -r to continue.
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -543,6 +543,25 @@ func (h *sessionHelper) handleCtl(input string) error {
|
|||
copy(data, h.sess.log)
|
||||
h.sess.mu.RUnlock()
|
||||
return h.saveTranscript(data)
|
||||
case "detach":
|
||||
if !h.sess.Core.Detach() {
|
||||
return fmt.Errorf("no running process to detach")
|
||||
}
|
||||
case "signal":
|
||||
if len(cmd) < 2 {
|
||||
return fmt.Errorf("usage: signal <pid> [TERM|KILL]")
|
||||
}
|
||||
pid, err := strconv.Atoi(cmd[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid pid: %s", cmd[1])
|
||||
}
|
||||
sig := 15 // SIGTERM default
|
||||
if len(cmd) >= 3 && strings.ToUpper(cmd[2]) == "KILL" {
|
||||
sig = 9
|
||||
}
|
||||
if err := h.sess.Core.SignalDetached(pid, sig); err != nil {
|
||||
return err
|
||||
}
|
||||
case "invalidate":
|
||||
if h.invalidateModels != nil {
|
||||
h.invalidateModels()
|
||||
|
|
|
|||
Reference in New Issue