140 lines
4.1 KiB
Go
140 lines
4.1 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package control
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
)
|
|
|
|
// A Screen server keeps its original environment after reattachment. Locate
|
|
// its current display processes through the server's open terminal devices,
|
|
// then use their reader ancestry. No Screen commands or init services are used.
|
|
// procRoot is explicit so device, ownership and ambiguity cases can be tested.
|
|
func attached_screen_reader(procRoot string, ancestors []int, uid int, readers []instance) (instance, bool, error) {
|
|
for _, pid := range ancestors {
|
|
processPath := filepath.Join(procRoot, strconv.Itoa(pid))
|
|
info, err := os.Stat(processPath)
|
|
if err != nil || !owned_by(info, uid) {
|
|
continue
|
|
}
|
|
command, err := os.ReadFile(filepath.Join(processPath, "cmdline"))
|
|
if err != nil || filepath.Base(strings.TrimSpace(string(bytes.SplitN(command, []byte{0}, 2)[0]))) != "SCREEN" {
|
|
continue
|
|
}
|
|
executablePath, err := os.Readlink(filepath.Join(processPath, "exe"))
|
|
name := strings.TrimSuffix(filepath.Base(executablePath), " (deleted)")
|
|
if err != nil || (name != "screen" && !strings.HasPrefix(name, "screen-")) {
|
|
continue
|
|
}
|
|
executable, err := os.Stat(filepath.Join(processPath, "exe"))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
reader, err := screen_display_reader(procRoot, processPath, uid, executable, readers)
|
|
return reader, true, err
|
|
}
|
|
return instance{}, false, nil
|
|
}
|
|
|
|
func owned_by(info os.FileInfo, uid int) bool {
|
|
stat, ok := info.Sys().(*syscall.Stat_t)
|
|
return ok && int(stat.Uid) == uid
|
|
}
|
|
|
|
func screen_display_reader(procRoot, serverPath string, uid int, executable os.FileInfo, readers []instance) (instance, error) {
|
|
fdPath := filepath.Join(serverPath, "fd")
|
|
files, err := os.ReadDir(fdPath)
|
|
if err != nil {
|
|
return instance{}, fmt.Errorf("cannot inspect Screen attachments: %w", err)
|
|
}
|
|
displays := map[uint32]bool{}
|
|
for _, file := range files {
|
|
path := filepath.Join(fdPath, file.Name())
|
|
info, err := os.Stat(path)
|
|
if err != nil || info.Mode()&os.ModeCharDevice == 0 {
|
|
continue
|
|
}
|
|
// /proc/PID/stat encodes tty_nr as a signed 32-bit device number.
|
|
stat, ok := info.Sys().(*syscall.Stat_t)
|
|
if ok && stat.Rdev != 0 {
|
|
displays[uint32(stat.Rdev)] = true
|
|
}
|
|
}
|
|
processes, err := os.ReadDir(procRoot)
|
|
if err != nil {
|
|
return instance{}, fmt.Errorf("cannot inspect Screen display processes: %w", err)
|
|
}
|
|
matches := map[int]instance{}
|
|
for _, process := range processes {
|
|
pid, err := strconv.Atoi(process.Name())
|
|
if err != nil || pid <= 1 {
|
|
continue
|
|
}
|
|
path := filepath.Join(procRoot, process.Name())
|
|
info, err := os.Stat(path)
|
|
if err != nil || !owned_by(info, uid) {
|
|
continue
|
|
}
|
|
data, err := os.ReadFile(filepath.Join(path, "stat"))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
_, tty := process_parent_tty(data)
|
|
if tty == 0 || !displays[tty] {
|
|
continue
|
|
}
|
|
info, err = os.Stat(filepath.Join(path, "exe"))
|
|
if err != nil || !os.SameFile(info, executable) {
|
|
continue
|
|
}
|
|
// Display clients and the server run the same Screen executable.
|
|
// The daemon has no controlling terminal; display clients have one.
|
|
seen := map[int]bool{}
|
|
for ancestor := pid; ancestor > 1 && !seen[ancestor]; {
|
|
seen[ancestor] = true
|
|
for _, reader := range readers {
|
|
if reader.PID == ancestor {
|
|
matches[reader.PID] = reader
|
|
ancestor = 0
|
|
break
|
|
}
|
|
}
|
|
if ancestor == 0 {
|
|
break
|
|
}
|
|
data, err := os.ReadFile(filepath.Join(procRoot, strconv.Itoa(ancestor), "stat"))
|
|
if err != nil {
|
|
break
|
|
}
|
|
ancestor, _ = process_parent_tty(data)
|
|
}
|
|
}
|
|
if len(matches) == 1 {
|
|
for _, reader := range matches {
|
|
return reader, nil
|
|
}
|
|
}
|
|
return instance{}, fmt.Errorf("Screen session does not identify a unique attached reader; use its private socket")
|
|
}
|
|
|
|
func process_parent_tty(data []byte) (int, uint32) {
|
|
end := bytes.LastIndexByte(data, ')')
|
|
if end < 0 {
|
|
return 0, 0
|
|
}
|
|
fields := strings.Fields(string(data[end+1:]))
|
|
if len(fields) < 5 {
|
|
return 0, 0
|
|
}
|
|
parent, _ := strconv.Atoi(fields[1])
|
|
tty, _ := strconv.ParseInt(fields[4], 10, 64)
|
|
return parent, uint32(tty)
|
|
}
|