162 lines
3.7 KiB
Go
162 lines
3.7 KiB
Go
package session
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
type XpraConfig struct {
|
|
Display int
|
|
WSPort int
|
|
BindAddr string
|
|
TunnelHost string
|
|
TunnelPort int
|
|
Username string
|
|
Password string
|
|
TmpDir string
|
|
WinBoxPath string
|
|
}
|
|
|
|
func StartXpra(cfg XpraConfig) (*os.Process, error) {
|
|
display := fmt.Sprintf(":%d", cfg.Display)
|
|
bindWS := fmt.Sprintf("%s:%d", cfg.BindAddr, cfg.WSPort)
|
|
winboxCmd := fmt.Sprintf("%s %s:%d %s %s",
|
|
cfg.WinBoxPath, cfg.TunnelHost, cfg.TunnelPort, cfg.Username, cfg.Password)
|
|
|
|
args := []string{
|
|
"start", display,
|
|
"--bind-ws=" + bindWS,
|
|
"--html=on",
|
|
"--daemon=no",
|
|
"--start-new-commands=no",
|
|
"--no-clipboard",
|
|
"--no-printing",
|
|
"--no-file-transfer",
|
|
"--no-notifications",
|
|
"--no-webcam",
|
|
"--no-speaker",
|
|
"--no-microphone",
|
|
"--sharing=no",
|
|
"--opengl=off",
|
|
"--env=XPRA_CLIENT_CAN_SHUTDOWN=0",
|
|
"--xvfb=Xvfb +extension GLX +extension Composite -screen 0 1280x800x24+32 -dpi 96 -nolisten tcp -noreset -auth /home/worker/.Xauthority",
|
|
"--start-child=" + winboxCmd,
|
|
}
|
|
|
|
logFile := filepath.Join(cfg.TmpDir, "xpra.log")
|
|
|
|
cmd := exec.Command("xpra", args...)
|
|
cmd.Dir = cfg.TmpDir
|
|
|
|
f, err := os.Create(logFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create xpra log: %w", err)
|
|
}
|
|
cmd.Stdout = f
|
|
cmd.Stderr = f
|
|
|
|
cmd.Env = append(os.Environ(),
|
|
"HOME="+cfg.TmpDir,
|
|
"DISPLAY="+display,
|
|
"XPRA_CLIENT_CAN_SHUTDOWN=0",
|
|
"LIBGL_ALWAYS_SOFTWARE=1",
|
|
"GALLIUM_DRIVER=llvmpipe",
|
|
)
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
return nil, fmt.Errorf("xpra start failed: %w", err)
|
|
}
|
|
|
|
return cmd.Process, nil
|
|
}
|
|
|
|
func WaitForXpraReady(ctx context.Context, bindAddr string, wsPort int, timeout time.Duration) error {
|
|
addr := fmt.Sprintf("%s:%d", bindAddr, wsPort)
|
|
deadline := time.After(timeout)
|
|
ticker := time.NewTicker(250 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-deadline:
|
|
return fmt.Errorf("xpra not ready after %s", timeout)
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-ticker.C:
|
|
conn, err := (&net.Dialer{Timeout: 200 * time.Millisecond}).DialContext(ctx, "tcp", addr)
|
|
if err == nil {
|
|
conn.Close()
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func QueryIdleTime(display int) int {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
defer cancel()
|
|
cmd := exec.CommandContext(ctx, "xpra", "info", fmt.Sprintf(":%d", display))
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
return -1
|
|
}
|
|
for _, line := range strings.Split(string(out), "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if strings.HasPrefix(line, "idle_time=") {
|
|
val := strings.TrimPrefix(line, "idle_time=")
|
|
if n, err := strconv.Atoi(val); err == nil {
|
|
return n
|
|
}
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func KillXpraSession(pid int) error {
|
|
if err := syscall.Kill(-pid, syscall.SIGTERM); err != nil {
|
|
slog.Warn("SIGTERM to xpra process group failed", "pid", pid, "err", err)
|
|
}
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
proc, err := os.FindProcess(pid)
|
|
if err == nil {
|
|
proc.Wait()
|
|
}
|
|
close(done)
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
return nil
|
|
case <-time.After(5 * time.Second):
|
|
slog.Warn("SIGKILL to xpra process group", "pid", pid)
|
|
return syscall.Kill(-pid, syscall.SIGKILL)
|
|
}
|
|
}
|
|
|
|
func CleanupTmpDir(dir string) error {
|
|
if dir == "" || !strings.HasPrefix(dir, "/tmp/winbox-sessions/") {
|
|
return fmt.Errorf("refusing to remove suspicious path: %s", dir)
|
|
}
|
|
return os.RemoveAll(dir)
|
|
}
|
|
|
|
func CreateSessionTmpDir(sessionID string) (string, error) {
|
|
dir := filepath.Join("/tmp/winbox-sessions", sessionID)
|
|
if err := os.MkdirAll(dir, 0700); err != nil {
|
|
return "", fmt.Errorf("create tmpdir: %w", err)
|
|
}
|
|
return dir, nil
|
|
}
|