vibetunnel/linux/pkg/session/control.go
Helmut Januschka a66620be52
fix: Go server CPU optimization & resize control flag (#32)
* fix: optimize Go server CPU usage from 500%+ to efficient levels

Major performance improvements to resolve excessive CPU consumption:

**Critical Fixes:**
- Remove WebSocket busy loop that caused continuous CPU spinning
- Fix microsecond-level polling (100μs → 10ms) reducing 100x operations
- Replace ps subprocess calls with efficient kill(pid,0) syscalls
- Increase timer intervals (1s → 30s) for session status checks

**Optimizations:**
- Control FIFO polling: 100ms → 1s intervals
- Select timeout: 100ms → 1s to reduce unnecessary wakeups
- Smart status caching: skip checks for already-exited sessions
- Remove unused imports (os/exec, strconv)

**Impact:**
- Eliminates tight loops causing 10,000+ operations per second
- Reduces subprocess overhead from frequent ps command executions
- Changes from polling-based to efficient event-driven architecture
- Expected CPU usage reduction from 500%+ to levels comparable with Node.js version

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: prevent WebSocket channel close panic with sync.Once

- Add sync.Once to prevent double-closing of done channel
- Update handleTextMessage signature to accept close function
- Use closeOnceFunc for safe channel closure across goroutines

Fixes panic: 'close of closed channel' in WebSocket handler

* feat: add --do-not-allow-column-set flag to disable resizing for spawned sessions

**New Flag:**
- `--do-not-allow-column-set` (default: true) - Disables terminal resizing for spawned shells
- Only affects sessions created with `spawn_terminal=true`
- Detached sessions (command-line, API without spawn) always allow resizing

**Implementation:**
- Add `IsSpawned` field to session.Config and session.Info structs
- Track whether session was spawned in terminal vs detached
- Server checks flag + spawn status before allowing resize operations
- Returns descriptive error for blocked resize attempts

**Behavior:**
- Spawned sessions: Resize blocked when flag enabled (default)
- Detached sessions: Always allow resizing regardless of flag
- Existing sessions preserve their resize capabilities

**API Response for blocked resize:**
```json
{
  "success": false,
  "message": "Resizing is disabled for spawned sessions",
  "error": "resize_disabled_for_spawned_sessions"
}
```

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat: extend --do-not-allow-column-set flag to block ALL session resizing

**Breaking Change:** Flag now affects both spawned AND detached sessions

**Changes:**
- Remove `sess.IsSpawned()` check in resize handler
- Block resizing for ALL sessions when flag is enabled (default: true)
- Update flag description: "Disable terminal resizing for all sessions (spawned and detached)"
- Update error message: "Terminal resizing is disabled by server configuration"
- Update error code: "resize_disabled_by_server"

**New Behavior:**
- `--do-not-allow-column-set=true` (default): NO resizing for any session type
- `--do-not-allow-column-set=false`: Allow resizing for all session types
- Applies uniformly to both spawned terminal windows and detached CLI sessions

**API Response for blocked resize:**
```json
{
  "success": false,
  "message": "Terminal resizing is disabled by server configuration",
  "error": "resize_disabled_by_server"
}
```

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-06-20 11:37:54 +02:00

134 lines
3.2 KiB
Go

package session
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"syscall"
"time"
)
// ControlCommand represents a command sent through the control FIFO
type ControlCommand struct {
Cmd string `json:"cmd"`
Cols int `json:"cols,omitempty"`
Rows int `json:"rows,omitempty"`
}
// createControlFIFO creates the control FIFO for a session
func (s *Session) createControlFIFO() error {
controlPath := filepath.Join(s.Path(), "control")
// Remove existing FIFO if it exists
if err := os.Remove(controlPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove existing control FIFO: %w", err)
}
// Create new FIFO
if err := syscall.Mkfifo(controlPath, 0600); err != nil {
return fmt.Errorf("failed to create control FIFO: %w", err)
}
debugLog("[DEBUG] Created control FIFO at %s", controlPath)
return nil
}
// startControlListener starts listening for control commands
func (s *Session) startControlListener() {
controlPath := filepath.Join(s.Path(), "control")
go func() {
for {
// Check if session is still running
s.mu.RLock()
if s.info.Status == string(StatusExited) {
s.mu.RUnlock()
break
}
s.mu.RUnlock()
// Open control FIFO in non-blocking mode
fd, err := syscall.Open(controlPath, syscall.O_RDONLY|syscall.O_NONBLOCK, 0)
if err != nil {
log.Printf("[ERROR] Failed to open control FIFO: %v", err)
time.Sleep(1 * time.Second)
continue
}
file := os.NewFile(uintptr(fd), controlPath)
decoder := json.NewDecoder(file)
// Read commands from FIFO
for {
var cmd ControlCommand
if err := decoder.Decode(&cmd); err != nil {
// Check if it's just EOF (no data available)
if err.Error() != "EOF" && err.Error() != "read /dev/stdin: resource temporarily unavailable" {
debugLog("[DEBUG] Control FIFO decode error: %v", err)
}
break
}
// Process command
s.handleControlCommand(&cmd)
}
file.Close()
// Longer delay before reopening to reduce CPU usage
time.Sleep(1 * time.Second)
}
debugLog("[DEBUG] Control listener stopped for session %s", s.ID[:8])
}()
}
// handleControlCommand processes a control command
func (s *Session) handleControlCommand(cmd *ControlCommand) {
debugLog("[DEBUG] Received control command for session %s: %+v", s.ID[:8], cmd)
switch cmd.Cmd {
case "resize":
if cmd.Cols > 0 && cmd.Rows > 0 {
if err := s.Resize(cmd.Cols, cmd.Rows); err != nil {
log.Printf("[ERROR] Failed to resize session %s: %v", s.ID[:8], err)
}
}
default:
log.Printf("[WARN] Unknown control command: %s", cmd.Cmd)
}
}
// SendControlCommand sends a command to a session's control FIFO
func SendControlCommand(sessionPath string, cmd *ControlCommand) error {
controlPath := filepath.Join(sessionPath, "control")
// Open FIFO with timeout
done := make(chan error, 1)
go func() {
file, err := os.OpenFile(controlPath, os.O_WRONLY, 0)
if err != nil {
done <- err
return
}
defer file.Close()
encoder := json.NewEncoder(file)
if err := encoder.Encode(cmd); err != nil {
done <- err
return
}
done <- nil
}()
// Wait with timeout
select {
case err := <-done:
return err
case <-time.After(1 * time.Second):
return fmt.Errorf("timeout sending control command")
}
}