diff --git a/linux/cmd/vibetunnel/main.go b/linux/cmd/vibetunnel/main.go index ea59d426..7eb9602a 100644 --- a/linux/cmd/vibetunnel/main.go +++ b/linux/cmd/vibetunnel/main.go @@ -288,6 +288,9 @@ func startServer(cfg *config.Config, manager *session.Manager) error { return fmt.Errorf("invalid port: %w", err) } + // Set the resize flag on the manager + manager.SetDoNotAllowColumnSet(doNotAllowColumnSet) + // Create and configure server server := api.NewServer(manager, staticPath, serverPassword, portInt) server.SetNoSpawn(noSpawn) diff --git a/linux/pkg/protocol/escape_parser_test.go b/linux/pkg/protocol/escape_parser_test.go index 4f72199b..7bdee504 100644 --- a/linux/pkg/protocol/escape_parser_test.go +++ b/linux/pkg/protocol/escape_parser_test.go @@ -162,7 +162,7 @@ func TestEscapeParser_Flush(t *testing.T) { parser := NewEscapeParser() // Process data with incomplete sequence - input := []byte("text\x1b[31") // incomplete CSI sequence + input := []byte("text\x1b[31") // incomplete CSI sequence processed, _ := parser.ProcessData(input) if !bytes.Equal(processed, []byte("text")) { diff --git a/linux/pkg/session/integration_test.go b/linux/pkg/session/integration_test.go index 0dda9c4b..b9b37120 100644 --- a/linux/pkg/session/integration_test.go +++ b/linux/pkg/session/integration_test.go @@ -21,7 +21,7 @@ func TestProcessTerminatorIntegration(t *testing.T) { } terminator := NewProcessTerminator(session) - + // Verify it was created with correct timeouts if terminator.gracefulTimeout != 3*time.Second { t.Errorf("Expected 3s graceful timeout, got %v", terminator.gracefulTimeout) @@ -34,16 +34,16 @@ func TestProcessTerminatorIntegration(t *testing.T) { func TestCustomErrorsIntegration(t *testing.T) { // Test custom error types err := NewSessionError("test error", ErrSessionNotFound, "test-id") - + if err.Code != ErrSessionNotFound { t.Errorf("Expected code %v, got %v", ErrSessionNotFound, err.Code) } - + if !IsSessionError(err, ErrSessionNotFound) { t.Error("IsSessionError should return true") } - + if GetSessionID(err) != "test-id" { t.Errorf("Expected session ID 'test-id', got '%s'", GetSessionID(err)) } -} \ No newline at end of file +} diff --git a/linux/pkg/session/manager.go b/linux/pkg/session/manager.go index 153d4e7b..6270880c 100644 --- a/linux/pkg/session/manager.go +++ b/linux/pkg/session/manager.go @@ -14,9 +14,10 @@ import ( ) type Manager struct { - controlPath string - runningSessions map[string]*Session - mutex sync.RWMutex + controlPath string + runningSessions map[string]*Session + mutex sync.RWMutex + doNotAllowColumnSet bool } func NewManager(controlPath string) *Manager { @@ -26,12 +27,26 @@ func NewManager(controlPath string) *Manager { } } +// SetDoNotAllowColumnSet sets the flag to disable terminal resizing for all sessions +func (m *Manager) SetDoNotAllowColumnSet(value bool) { + m.mutex.Lock() + defer m.mutex.Unlock() + m.doNotAllowColumnSet = value +} + +// GetDoNotAllowColumnSet returns the current value of the resize disable flag +func (m *Manager) GetDoNotAllowColumnSet() bool { + m.mutex.RLock() + defer m.mutex.RUnlock() + return m.doNotAllowColumnSet +} + func (m *Manager) CreateSession(config Config) (*Session, error) { if err := os.MkdirAll(m.controlPath, 0755); err != nil { return nil, fmt.Errorf("failed to create control directory: %w", err) } - session, err := newSession(m.controlPath, config) + session, err := newSession(m.controlPath, config, m) if err != nil { return nil, err } @@ -56,7 +71,7 @@ func (m *Manager) CreateSessionWithID(id string, config Config) (*Session, error return nil, fmt.Errorf("failed to create control directory: %w", err) } - session, err := newSessionWithID(m.controlPath, id, config) + session, err := newSessionWithID(m.controlPath, id, config, m) if err != nil { return nil, err } @@ -86,7 +101,7 @@ func (m *Manager) GetSession(id string) (*Session, error) { m.mutex.RUnlock() // Fall back to loading from disk (for sessions that might have been started before this manager instance) - return loadSession(m.controlPath, id) + return loadSession(m.controlPath, id, m) } func (m *Manager) FindSession(nameOrID string) (*Session, error) { @@ -119,7 +134,7 @@ func (m *Manager) ListSessions() ([]*Info, error) { continue } - session, err := loadSession(m.controlPath, entry.Name()) + session, err := loadSession(m.controlPath, entry.Name(), m) if err != nil { // Log the error when we can't load a session if os.Getenv("VIBETUNNEL_DEBUG") != "" { diff --git a/linux/pkg/session/pty.go b/linux/pkg/session/pty.go index bf924fa9..bc66e07a 100644 --- a/linux/pkg/session/pty.go +++ b/linux/pkg/session/pty.go @@ -257,6 +257,12 @@ func (p *PTY) Run() error { // Handle SIGWINCH in a separate goroutine go func() { for range winchCh { + // Check if resizing is disabled globally + if p.session.manager != nil && p.session.manager.GetDoNotAllowColumnSet() { + debugLog("[DEBUG] PTY.Run: Received SIGWINCH but resizing is disabled by server configuration") + continue + } + // Get current terminal size if we're attached to a terminal if term.IsTerminal(int(os.Stdin.Fd())) { width, height, err := term.GetSize(int(os.Stdin.Fd())) @@ -434,6 +440,11 @@ func (p *PTY) Attach() error { signal.Notify(ch, syscall.SIGWINCH) go func() { for range ch { + // Check if resizing is disabled globally + if p.session.manager != nil && p.session.manager.GetDoNotAllowColumnSet() { + debugLog("[DEBUG] PTY.Attach: Received SIGWINCH but resizing is disabled by server configuration") + continue + } if err := p.updateSize(); err != nil { log.Printf("[ERROR] PTY.Attach: Failed to update size: %v", err) } @@ -441,8 +452,13 @@ func (p *PTY) Attach() error { }() defer signal.Stop(ch) - if err := p.updateSize(); err != nil { - log.Printf("[ERROR] PTY.Attach: Failed to update initial size: %v", err) + // Only update size initially if resizing is allowed + if p.session.manager == nil || !p.session.manager.GetDoNotAllowColumnSet() { + if err := p.updateSize(); err != nil { + log.Printf("[ERROR] PTY.Attach: Failed to update initial size: %v", err) + } + } else { + debugLog("[DEBUG] PTY.Attach: Skipping initial resize - resizing is disabled by server configuration") } errCh := make(chan error, 2) diff --git a/linux/pkg/session/session.go b/linux/pkg/session/session.go index e21ee1cf..e2402527 100644 --- a/linux/pkg/session/session.go +++ b/linux/pkg/session/session.go @@ -67,14 +67,15 @@ type Session struct { stdinPipe *os.File stdinMutex sync.Mutex mu sync.RWMutex + manager *Manager // Reference to manager for accessing global settings } -func newSession(controlPath string, config Config) (*Session, error) { +func newSession(controlPath string, config Config, manager *Manager) (*Session, error) { id := uuid.New().String() - return newSessionWithID(controlPath, id, config) + return newSessionWithID(controlPath, id, config, manager) } -func newSessionWithID(controlPath string, id string, config Config) (*Session, error) { +func newSessionWithID(controlPath string, id string, config Config, manager *Manager) (*Session, error) { sessionPath := filepath.Join(controlPath, id) // Only log in debug mode @@ -159,10 +160,11 @@ func newSessionWithID(controlPath string, id string, config Config) (*Session, e ID: id, controlPath: controlPath, info: info, + manager: manager, }, nil } -func loadSession(controlPath, id string) (*Session, error) { +func loadSession(controlPath, id string, manager *Manager) (*Session, error) { sessionPath := filepath.Join(controlPath, id) info, err := LoadInfo(sessionPath) if err != nil { @@ -173,6 +175,7 @@ func loadSession(controlPath, id string) (*Session, error) { ID: id, controlPath: controlPath, info: info, + manager: manager, } // Validate that essential session files exist @@ -437,8 +440,9 @@ func (s *Session) cleanup() { } func (s *Session) Resize(width, height int) error { - if s.pty == nil { - return NewSessionError("session not started", ErrSessionNotRunning, s.ID) + // Check if resizing is disabled globally + if s.manager != nil && s.manager.GetDoNotAllowColumnSet() { + return NewSessionError("terminal resizing is disabled by server configuration", ErrInvalidInput, s.ID) } // Check if session is still alive @@ -464,7 +468,20 @@ func (s *Session) Resize(width, height int) error { log.Printf("[ERROR] Failed to save session info after resize: %v", err) } - // Resize the PTY + // If this is a spawned session, send resize command through control FIFO + if s.IsSpawned() { + cmd := &ControlCommand{ + Cmd: "resize", + Cols: width, + Rows: height, + } + return SendControlCommand(s.Path(), cmd) + } + + // For non-spawned sessions, resize the PTY directly + if s.pty == nil { + return NewSessionError("session not started", ErrSessionNotRunning, s.ID) + } return s.pty.Resize(width, height) } diff --git a/linux/pkg/session/session_test.go b/linux/pkg/session/session_test.go index a7136c92..c659d927 100644 --- a/linux/pkg/session/session_test.go +++ b/linux/pkg/session/session_test.go @@ -14,6 +14,9 @@ func TestNewSession(t *testing.T) { tmpDir := t.TempDir() controlPath := filepath.Join(tmpDir, "control") + // Create a test manager + manager := NewManager(controlPath) + config := &Config{ Name: "test-session", Cmdline: []string{"/bin/sh", "-c", "echo test"}, @@ -22,7 +25,7 @@ func TestNewSession(t *testing.T) { Height: 24, } - session, err := newSession(controlPath, *config) + session, err := newSession(controlPath, *config, manager) if err != nil { t.Fatalf("newSession() error = %v", err) } @@ -60,10 +63,13 @@ func TestNewSession_Defaults(t *testing.T) { tmpDir := t.TempDir() controlPath := filepath.Join(tmpDir, "control") + // Create a test manager + manager := NewManager(controlPath) + // Minimal config config := &Config{} - session, err := newSession(controlPath, *config) + session, err := newSession(controlPath, *config, manager) if err != nil { t.Fatalf("newSession() error = %v", err) } diff --git a/linux/pkg/session/terminal_simple_test.go b/linux/pkg/session/terminal_simple_test.go index bea0b347..04dfa5a0 100644 --- a/linux/pkg/session/terminal_simple_test.go +++ b/linux/pkg/session/terminal_simple_test.go @@ -73,4 +73,4 @@ func TestIsTerminal(t *testing.T) { } }) } -} \ No newline at end of file +}